Merge branch 'MDL-38301-master' of git://github.com/junpataleta/moodle
authorDavid Monllao <davidm@moodle.com>
Mon, 3 Sep 2018 10:44:50 +0000 (12:44 +0200)
committerDavid Monllao <davidm@moodle.com>
Mon, 3 Sep 2018 10:44:50 +0000 (12:44 +0200)
155 files changed:
admin/roles/classes/define_role_table_advanced.php
admin/tool/dataprivacy/amd/build/categoriesactions.min.js
admin/tool/dataprivacy/amd/build/purposesactions.min.js
admin/tool/dataprivacy/amd/src/categoriesactions.js
admin/tool/dataprivacy/amd/src/purposesactions.js
admin/tool/dataprivacy/classes/data_registry.php
admin/tool/dataprivacy/classes/metadata_registry.php
admin/tool/dataprivacy/classes/output/data_registry_page.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/mydatarequests.php
admin/tool/dataprivacy/templates/categories.mustache
admin/tool/dataprivacy/templates/component_status.mustache
admin/tool/dataprivacy/templates/data_registry_compliance.mustache
admin/tool/dataprivacy/templates/purposes.mustache
admin/tool/dataprivacy/tests/behat/manage_categories.feature [new file with mode: 0644]
admin/tool/dataprivacy/tests/behat/manage_purposes.feature [new file with mode: 0644]
admin/tool/policy/amd/build/policyactions.min.js
admin/tool/policy/amd/src/policyactions.js
admin/tool/policy/styles.css
admin/tool/policy/tests/behat/consent.feature
admin/tool/recyclebin/tests/category_bin_test.php
admin/tool/uploadcourse/classes/helper.php
admin/tool/uploadcourse/classes/step2_form.php
admin/tool/uploadcourse/cli/uploadcourse.php
admin/tool/uploadcourse/index.php
admin/tool/usertours/classes/local/filter/category.php
badges/criteria/award_criteria_courseset.php
blocks/course_list/block_course_list.php
blocks/myoverview/classes/output/courses_view.php
blocks/recent_activity/classes/task/cleanup.php
blocks/rss_client/block_rss_client.php
blocks/rss_client/classes/task/refreshfeeds.php
blocks/rss_client/tests/cron_test.php
blog/lib.php
cache/classes/helper.php
cache/classes/loaders.php
cache/tests/cache_test.php
cache/tests/fixtures/lib.php
cache/upgrade.txt
calendar/classes/local/event/container.php
calendar/classes/local/event/data_access/event_vault.php
calendar/classes/local/event/factories/event_abstract_factory.php
calendar/classes/local/event/forms/eventtype.php
calendar/classes/local/event/proxies/coursecat_proxy.php
calendar/externallib.php
calendar/lib.php
calendar/managesubscriptions.php
calendar/tests/calendar_information_test.php
calendar/tests/coursecat_proxy_test.php
cohort/edit_form.php
cohort/lib.php
cohort/upload_form.php
course/ajax/management.php
course/classes/category.php [new file with mode: 0644]
course/classes/deletecategory_form.php
course/classes/editcategory_form.php
course/classes/list_element.php [new file with mode: 0644]
course/classes/management/helper.php
course/classes/management_renderer.php
course/completion_form.php
course/edit_form.php
course/editcategory.php
course/externallib.php
course/index.php
course/lib.php
course/management.php
course/renderer.php
course/request_form.php
course/search.php
course/tests/behat/behat_course.php
course/tests/category_test.php [moved from lib/tests/coursecatlib_test.php with 80% similarity]
course/tests/courselib_test.php
course/tests/management_helper_test.php
enrol/imsenterprise/lib.php
enrol/imsenterprise/tests/imsenterprise_test.php
enrol/self/classes/deleteselectedusers_operation.php
grade/grading/classes/privacy/gradingform_legacy_polyfill.php
grade/grading/classes/privacy/gradingform_provider.php
grade/grading/classes/privacy/gradingform_provider_v2.php [new file with mode: 0644]
grade/grading/classes/privacy/provider.php
grade/grading/form/guide/classes/privacy/provider.php
grade/grading/form/guide/lang/en/gradingform_guide.php
grade/grading/form/guide/tests/privacy_test.php
grade/grading/form/rubric/classes/privacy/provider.php
grade/grading/form/rubric/lang/en/gradingform_rubric.php
grade/grading/form/rubric/tests/privacy_test.php [new file with mode: 0644]
grade/grading/form/upgrade.txt
grade/grading/tests/fixtures/marking_guide.php [new file with mode: 0644]
grade/grading/tests/privacy_legacy_polyfill_test.php
grade/grading/tests/privacy_test.php
grade/lib.php
grade/report/grader/index.php
grade/report/lib.php
grade/report/overview/index.php
grade/report/overview/lib.php
grade/report/user/index.php
grade/upgrade.txt [new file with mode: 0644]
install/lang/ca/install.php
lang/en/error.php
lib/accesslib.php
lib/amd/build/ajax.min.js
lib/amd/build/modal.min.js
lib/amd/src/ajax.js
lib/amd/src/modal.js
lib/bennu/iCalendar_components.php
lib/bennu/iCalendar_properties.php
lib/bennu/readme_moodle.txt
lib/classes/event/course_category_deleted.php
lib/classes/external/coursecat_summary_exporter.php
lib/coursecatlib.php
lib/db/renamedclasses.php
lib/db/upgrade.php
lib/deprecatedlib.php
lib/editor/atto/lib.php
lib/enrollib.php
lib/filebrowser/tests/file_browser_test.php
lib/moodlelib.php
lib/navigationlib.php
lib/questionlib.php
lib/testing/generator/data_generator.php
lib/tests/gradelib_test.php
lib/tests/questionlib_test.php
lib/upgrade.txt
message/classes/api.php
mod/assign/classes/privacy/provider.php
mod/assign/feedback/editpdf/lib.php
mod/assign/feedback/file/lib.php
mod/assign/gradingtable.php
mod/assign/tests/behat/group_submission.feature
privacy/classes/local/deprecated.php [new file with mode: 0644]
question/format.php
question/format/gift/tests/behat/import_export.feature
question/format/xml/tests/behat/import_export.feature
question/import.php
question/type/ddimageortext/tests/behat/export.feature
question/type/ddmarker/tests/behat/export.feature
question/type/ddwtos/tests/behat/export.feature
question/type/description/tests/behat/export.feature
question/type/essay/tests/behat/export.feature
question/type/gapselect/tests/behat/import_test.feature
question/type/match/tests/behat/export.feature
question/type/multichoice/tests/behat/export.feature
question/type/shortanswer/tests/behat/export.feature
question/type/truefalse/tests/behat/export.feature
tag/classes/tag.php
tag/tests/taglib_test.php
theme/boost/classes/output/core_course/management/renderer.php
theme/boost/scss/moodle/core.scss
theme/boost/style/moodle.css
user/externallib.php
user/filters/courserole.php
user/lib.php
user/tests/externallib_test.php
user/tests/userlib_test.php
version.php

index 354ba8c..d5f6c18 100644 (file)
@@ -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.
index 66e3b0a..cac33a4 100644 (file)
Binary files a/admin/tool/dataprivacy/amd/build/categoriesactions.min.js and b/admin/tool/dataprivacy/amd/build/categoriesactions.min.js differ
index 0b15981..33b773c 100644 (file)
Binary files a/admin/tool/dataprivacy/amd/build/purposesactions.min.js and b/admin/tool/dataprivacy/amd/build/purposesactions.min.js differ
index c40a1a7..6d05977 100644 (file)
@@ -58,25 +58,28 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents) {
             var stringkeys = [
                 {
                     key: 'deletecategory',
-                    component: 'tool_dataprivacy',
-                    param: categoryname
+                    component: 'tool_dataprivacy'
                 },
                 {
                     key: 'deletecategorytext',
                     component: 'tool_dataprivacy',
                     param: categoryname
+                },
+                {
+                    key: 'delete'
                 }
             ];
 
             Str.get_strings(stringkeys).then(function(langStrings) {
                 var title = langStrings[0];
                 var confirmMessage = langStrings[1];
+                var buttonText = langStrings[2];
                 return ModalFactory.create({
                     title: title,
                     body: confirmMessage,
                     type: ModalFactory.types.SAVE_CANCEL
                 }).then(function(modal) {
-                    modal.setSaveButtonText(title);
+                    modal.setSaveButtonText(buttonText);
 
                     // Handle save event.
                     modal.getRoot().on(ModalEvents.save, function() {
index fd92141..05abf71 100644 (file)
@@ -58,25 +58,28 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents) {
             var stringkeys = [
                 {
                     key: 'deletepurpose',
-                    component: 'tool_dataprivacy',
-                    param: purposename
+                    component: 'tool_dataprivacy'
                 },
                 {
                     key: 'deletepurposetext',
                     component: 'tool_dataprivacy',
                     param: purposename
+                },
+                {
+                    key: 'delete'
                 }
             ];
 
             Str.get_strings(stringkeys).then(function(langStrings) {
                 var title = langStrings[0];
                 var confirmMessage = langStrings[1];
+                var buttonText = langStrings[2];
                 return ModalFactory.create({
                     title: title,
                     body: confirmMessage,
                     type: ModalFactory.types.SAVE_CANCEL
                 }).then(function(modal) {
-                    modal.setSaveButtonText(title);
+                    modal.setSaveButtonText(buttonText);
 
                     // Handle save event.
                     modal.getRoot().on(ModalEvents.save, function() {
index dbc8f32..1fac2f5 100644 (file)
@@ -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) {
index ca50157..e29174a 100644 (file)
@@ -76,6 +76,16 @@ class metadata_registry {
                 if (isset($contributedplugins[$plugintype][$shortname])) {
                     $internaldata['external'] = true;
                 }
+
+                // Check if the interface is deprecated.
+                if (!$manager->is_empty_subsystem($component)) {
+                    $classname = $manager->get_provider_classname_for_component($component);
+                    $componentclass = new $classname();
+                    if ($componentclass instanceof \core_privacy\local\deprecated) {
+                        $internaldata['deprecated'] = true;
+                    }
+                }
+
                 return $internaldata;
             }, $leaves['plugins']);
             $fullyrichtree[$branch]['plugin_type_raw'] = $plugintype;
index cfdad43..8d5fd35 100644 (file)
@@ -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 = [];
index a50a271..4fa38fa 100644 (file)
@@ -77,15 +77,17 @@ $string['datecomment'] = '[{$a->date}]: ' . PHP_EOL . ' {$a->comment}';
 $string['daterequested'] = 'Date requested';
 $string['daterequesteddetail'] = 'Date requested:';
 $string['defaultsinfo'] = 'Default categories and purposes are applied to all newly created instances.';
-$string['deletecategory'] = 'Delete "{$a}" category';
-$string['deletecategorytext'] = 'Are you sure you want to delete "{$a}" category?';
+$string['deletecategory'] = 'Delete category';
+$string['deletecategorytext'] = 'Are you sure you want to delete the category \'{$a}\'?';
 $string['deleteexpiredcontextstask'] = 'Delete expired contexts';
 $string['deleteexpireddatarequeststask'] = 'Delete files from completed data requests that have expired';
-$string['deletepurpose'] = 'Delete "{$a}" purpose';
-$string['deletepurposetext'] = 'Are you sure you want to delete "{$a}" purpose?';
+$string['deletepurpose'] = 'Delete purpose';
+$string['deletepurposetext'] = 'Are you sure you want to delete the purpose \'{$a}\'?';
 $string['defaultssaved'] = 'Defaults saved';
 $string['deny'] = 'Deny';
 $string['denyrequest'] = 'Deny request';
+$string['deprecated'] = 'Deprecated';
+$string['deprecatedexplanation'] = 'This plugin is using an old version of one of the privacy interfaces and should be updated.';
 $string['download'] = 'Download';
 $string['downloadexpireduser'] = 'Download has expired. Submit a new request if you wish to export your personal data.';
 $string['dporolemapping'] = 'Privacy officer role mapping';
index 2568096..f435d00 100644 (file)
@@ -55,7 +55,7 @@ $PAGE->set_title($title);
 echo $OUTPUT->header();
 echo $OUTPUT->heading($title);
 
-$requests = tool_dataprivacy\api::get_data_requests($USER->id);
+$requests = tool_dataprivacy\api::get_data_requests($USER->id, [], [], 'timecreated DESC');
 $requestlist = new tool_dataprivacy\output\my_data_requests_page($requests);
 $requestlistoutput = $PAGE->get_renderer('tool_dataprivacy');
 echo $requestlistoutput->render($requestlist);
index 5bf63fb..ef31041 100644 (file)
@@ -53,7 +53,7 @@
 <div data-region="categories" class="m-t-3 m-b-1">
     <h3>{{#str}}categories, tool_dataprivacy{{/str}}</h3>
     <div class="m-y-1">
-        <button class="btn btn-secondary" data-add-element="category">
+        <button class="btn btn-secondary" data-add-element="category" title="{{#str}}addcategory, tool_dataprivacy{{/str}}">
             {{#pix}}t/add, moodle, {{#str}}addcategory, tool_dataprivacy{{/str}}{{/pix}}
         </button>
     </div>
index c62dc0b..3f7b691 100644 (file)
@@ -61,6 +61,9 @@
         {{#external}}
             <span class="badge badge-pill badge-notice">{{#str}}external, tool_dataprivacy{{/str}}</span>
         {{/external}}
+        {{#deprecated}}
+            <span class="badge badge-pill badge-warning">{{#str}}deprecated, tool_dataprivacy{{/str}}</span>
+        {{/deprecated}}
     </div>
 
     {{#compliant}}
index caadac1..c4a9c1b 100644 (file)
@@ -45,6 +45,8 @@
         <dd>{{#str}}requiresattentionexplanation, tool_dataprivacy{{/str}}</dd>
         <dt><span class="badge badge-pill badge-notice">{{#str}}external, tool_dataprivacy{{/str}}</span></dt>
         <dd>{{#str}}externalexplanation, tool_dataprivacy{{/str}}</dd>
+        <dt><span class="badge badge-pill badge-warning">{{#str}}deprecated, tool_dataprivacy{{/str}}</span></dt>
+        <dd>{{#str}}deprecatedexplanation, tool_dataprivacy{{/str}}</dd>
     </dl>
     <hr />
     <div class="clearfix"><a class="tool_dataprivacy-expand-all pull-right" href="#" data-visibility-state='visible'>{{#str}}visible, tool_dataprivacy{{/str}}</a></div>
index 6e6c855..4461eab 100644 (file)
@@ -60,7 +60,7 @@
 <div data-region="purposes" class="m-t-3 m-b-1">
     <h3>{{#str}}purposes, tool_dataprivacy{{/str}}</h3>
     <div class="m-y-1">
-        <button class="btn btn-secondary" data-add-element="purpose">
+        <button class="btn btn-secondary" data-add-element="purpose" title="{{#str}}addpurpose, tool_dataprivacy{{/str}}">
             {{#pix}}t/add, moodle, {{#str}}addpurpose, tool_dataprivacy{{/str}}{{/pix}}
         </button>
     </div>
diff --git a/admin/tool/dataprivacy/tests/behat/manage_categories.feature b/admin/tool/dataprivacy/tests/behat/manage_categories.feature
new file mode 100644 (file)
index 0000000..9952bbb
--- /dev/null
@@ -0,0 +1,34 @@
+@tool @tool_dataprivacy @javascript
+Feature: Manage data categories
+  As the privacy officer
+  In order to manage the data registry
+  I need to be able to manage the data categories for the data registry
+
+  Background:
+    Given I log in as "admin"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Edit" "link"
+    And I choose "Categories" in the open action menu
+    And I press "Add category"
+    And I set the field "Name" to "Category 1"
+    And I set the field "Description" to "Category 1 description"
+    When I press "Save"
+    Then I should see "Category 1" in the "List of data categories" "table"
+    And I should see "Category 1 description" in the "Category 1" "table_row"
+
+  Scenario: Update a data category
+    Given I click on "Actions" "link" in the "Category 1" "table_row"
+    And I choose "Edit" in the open action menu
+    And I set the field "Name" to "Category 1 edited"
+    And I set the field "Description" to "Category 1 description edited"
+    When I press "Save changes"
+    Then I should see "Category 1 edited" in the "List of data categories" "table"
+    And I should see "Category 1 description edited" in the "List of data categories" "table"
+
+  Scenario: Delete a data category
+    Given I click on "Actions" "link" in the "Category 1" "table_row"
+    And I choose "Delete" in the open action menu
+    And I should see "Delete category"
+    And I should see "Are you sure you want to delete the category 'Category 1'?"
+    When I press "Delete"
+    Then I should not see "Category 1" in the "List of data categories" "table"
diff --git a/admin/tool/dataprivacy/tests/behat/manage_purposes.feature b/admin/tool/dataprivacy/tests/behat/manage_purposes.feature
new file mode 100644 (file)
index 0000000..b236d0a
--- /dev/null
@@ -0,0 +1,56 @@
+@tool @tool_dataprivacy @javascript
+Feature: Manage data storage purposes
+  As the privacy officer
+  In order to manage the data registry
+  I need to be able to manage the data storage purposes for the data registry
+
+  Background:
+    Given I log in as "admin"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Edit" "link"
+    And I choose "Purposes" in the open action menu
+    And I press "Add purpose"
+    And I set the field "Name" to "Purpose 1"
+    And I set the field "Description" to "Purpose 1 description"
+    And I click on ".form-autocomplete-downarrow" "css_element" in the "Lawful bases" "form_row"
+    And I click on "Contract (GDPR Art. 6.1(b))" "list_item"
+    And I click on "Legal obligation (GDPR Art 6.1(c))" "list_item"
+    And I press key "27" in the field "Lawful bases"
+    And I click on ".form-autocomplete-downarrow" "css_element" in the "Sensitive personal data processing reasons" "form_row"
+    And I click on "Explicit consent (GDPR Art. 9.2(a))" "list_item"
+    And I press key "27" in the field "Sensitive personal data processing reasons"
+    And I set the field "retentionperiodnumber" to "2"
+    When I press "Save"
+    Then I should see "Purpose 1" in the "List of data purposes" "table"
+    And I should see "Contract (GDPR Art. 6.1(b))" in the "Purpose 1" "table_row"
+    And I should see "Legal obligation (GDPR Art 6.1(c))" in the "Purpose 1" "table_row"
+    And I should see "Explicit consent (GDPR Art. 9.2(a))" in the "Purpose 1" "table_row"
+    And I should see "2 years" in the "Purpose 1" "table_row"
+    And I should see "No" in the "Purpose 1" "table_row"
+
+  Scenario: Update a data storage purpose
+    Given I click on "Actions" "link" in the "Purpose 1" "table_row"
+    And I choose "Edit" in the open action menu
+    And I set the field "Name" to "Purpose 1 edited"
+    And I set the field "Description" to "Purpose 1 description edited"
+    And I click on "Legal obligation (GDPR Art 6.1(c))" "text" in the ".form-autocomplete-selection" "css_element"
+    And I click on ".form-autocomplete-downarrow" "css_element" in the "Lawful bases" "form_row"
+    And I click on "Vital interests (GDPR Art. 6.1(d))" "list_item"
+    And I press key "27" in the field "Lawful bases"
+    And I set the field "retentionperiodnumber" to "3"
+    And I click on "protected" "checkbox"
+    When I press "Save changes"
+    Then I should see "Purpose 1 edited" in the "List of data purposes" "table"
+    And I should see "Purpose 1 description edited" in the "Purpose 1 edited" "table_row"
+    And I should see "Vital interests (GDPR Art. 6.1(d))" in the "Purpose 1 edited" "table_row"
+    And I should see "3 years" in the "Purpose 1 edited" "table_row"
+    But I should not see "Legal obligation (GDPR Art 6.1(c))" in the "Purpose 1 edited" "table_row"
+    And I should not see "No" in the "Purpose 1 edited" "table_row"
+
+  Scenario: Delete a data storage purpose
+    Given I click on "Actions" "link" in the "Purpose 1" "table_row"
+    And I choose "Delete" in the open action menu
+    And I should see "Delete purpose"
+    And I should see "Are you sure you want to delete the purpose 'Purpose 1'?"
+    When I press "Delete"
+    Then I should not see "Purpose 1" in the "List of data purposes" "table"
index 9a87001..2e54773 100644 (file)
Binary files a/admin/tool/policy/amd/build/policyactions.min.js and b/admin/tool/policy/amd/build/policyactions.min.js differ
index ae70f48..b3de54a 100644 (file)
@@ -65,43 +65,55 @@ function($, Ajax, Notification, ModalFactory, ModalEvents) {
                 args: params
             };
 
+            var modalTitle = $.Deferred();
+            var modalBody = $.Deferred();
+
+            var modal = ModalFactory.create({
+                title: modalTitle,
+                body: modalBody,
+                large: true
+            })
+            .then(function(modal) {
+                // Handle hidden event.
+                modal.getRoot().on(ModalEvents.hidden, function() {
+                    // Destroy when hidden.
+                    modal.destroy();
+                });
+
+                return modal;
+            })
+            .then(function(modal) {
+                modal.show();
+
+                return modal;
+            })
+            .catch(Notification.exception);
+
+            // Make the request now that the modal is configured.
             var promises = Ajax.call([request]);
-            var modalTitle = '';
-            var modalType = ModalFactory.types.DEFAULT;
             $.when(promises[0]).then(function(data) {
                 if (data.result.policy) {
-                    modalTitle = data.result.policy.name;
-                    return data.result.policy.content;
+                    modalTitle.resolve(data.result.policy.name);
+                    modalBody.resolve(data.result.policy.content);
+
+                    return data;
+                } else {
+                    throw new Error(data.warnings[0].message);
                 }
-                // Fail.
-                Notification.addNotification({
-                    message: data.warnings[0].message,
+            }).catch(function(message) {
+                modal.then(function(modal) {
+                    modal.hide();
+                    modal.destroy();
+
+                    return modal;
+                })
+                .catch(Notification.exception);
+
+                return Notification.addNotification({
+                    message: message,
                     type: 'error'
                 });
-                return false;
-
-            }).then(function(html) {
-                if (html != false) {
-                    return ModalFactory.create({
-                        title: modalTitle,
-                        body: html,
-                        type: modalType,
-                        large: true
-                    }).then(function(modal) {
-                        // Handle hidden event.
-                        modal.getRoot().on(ModalEvents.hidden, function() {
-                            // Destroy when hidden.
-                            modal.destroy();
-                        });
-
-                        return modal;
-                    });
-                }
-                return false;
-            }).done(function(modal) {
-                // Show the modal.
-                modal.show();
-            }).fail(Notification.exception);
+            });
         });
 
     };
index 5bebc1a..52bf795 100644 (file)
     z-index: 9999999;
 }
 
+.behat-site .eupopup-container-bottom {
+    position: relative;
+}
+
 .eupopup-container-bottom {
     position: fixed;
     bottom: 0;
index a0065c1..047ac5e 100644 (file)
@@ -460,7 +460,6 @@ Feature: User must accept policy managed by this plugin when logging in and sign
       | This privacy policy | 1    |          | full text3 | short text3 | active   | loggedin |
       | This guests policy  | 0    |          | full text4 | short text4 | active   | guest    |
     And I am on site homepage
-    And I change window size to "large"
     And I follow "Log in"
     When I press "Log in as a guest"
     Then I should see "If you continue browsing this website, you agree to our policies"
index 22c7f21..bada3d6 100644 (file)
@@ -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.
index 370d13a..2e514a7 100644 (file)
@@ -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 {
index 2e39463..89ee15d 100644 (file)
@@ -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');
 
index 75c39d8..1e9394d 100644 (file)
@@ -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',
index efeebf9..69f4410 100644 (file)
@@ -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');
index 5f7ffc5..0b1f119 100644 (file)
@@ -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;
     }
 
index 52b62b0..f60715c 100644 (file)
@@ -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();
index 10cbf23..9d09dcb 100644 (file)
@@ -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) {
index b00741d..2c6027e 100644 (file)
@@ -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) {
index c3f34c7..13dc420 100644 (file)
@@ -50,9 +50,12 @@ class cleanup extends \core\task\scheduled_task {
      * Remove old entries from table block_recent_activity
      */
     public function execute() {
-        global $DB;
+        global $CFG, $DB;
+        require_once("{$CFG->dirroot}/course/lib.php");
+
         // Those entries will never be displayed as RECENT anyway.
-        $DB->delete_records_select('block_recent_activity', 'timecreated < ?',
-            array(time() - COURSE_MAX_RECENT_PERIOD));
+        $DB->delete_records_select('block_recent_activity', 'timecreated < ?', [
+                time() - COURSE_MAX_RECENT_PERIOD,
+            ]);
     }
 }
index 2a2fd78..3b33049 100644 (file)
@@ -30,9 +30,6 @@
  */
 
  class block_rss_client extends block_base {
-    /** The maximum time in seconds that cron will wait between attempts to retry failing RSS feeds. */
-    const CLIENT_MAX_SKIPTIME = 43200; // 60 * 60 * 12 seconds.
-
     /** @var bool track whether any of the output feeds have recorded failures */
     private $hasfailedfeeds = false;
 
             return core_text::substr($title, 0, $max - 3) . '...';
         }
     }
-
-    /**
-     * Calculates a new skip time for a record based on the current skip time.
-     *
-     * @param int $currentskip The curreent skip time of a record.
-     * @return int A new skip time that should be set.
-     */
-    public function calculate_skiptime($currentskip) {
-        // The default time to skiptime.
-        $newskiptime = $this->cron * 1.1;
-        if ($currentskip > 0) {
-            // Double the last time.
-            $newskiptime = $currentskip * 2;
-        }
-        if ($newskiptime > self::CLIENT_MAX_SKIPTIME) {
-            // Do not allow the skip time to increase indefinatly.
-            $newskiptime = self::CLIENT_MAX_SKIPTIME;
-        }
-        return $newskiptime;
-    }
 }
index 3e34e1b..3e12ab2 100644 (file)
@@ -37,6 +37,9 @@ defined('MOODLE_INTERNAL') || die();
  */
 class refreshfeeds extends \core\task\scheduled_task {
 
+    /** The maximum time in seconds that cron will wait between attempts to retry failing RSS feeds. */
+    const CLIENT_MAX_SKIPTIME = HOURSECS * 12;
+
     /**
      * Name for this task.
      *
@@ -65,7 +68,7 @@ class refreshfeeds extends \core\task\scheduled_task {
      */
     public function execute() {
         global $CFG, $DB;
-        require_once($CFG->libdir.'/simplepie/moodle_simplepie.php');
+        require_once("{$CFG->libdir}/simplepie/moodle_simplepie.php");
 
         // We are going to measure execution times.
         $starttime = microtime();
@@ -84,22 +87,11 @@ class refreshfeeds extends \core\task\scheduled_task {
                 continue;
             }
 
-            // Fetch the rss feed, using standard simplepie caching
-            // so feeds will be renewed only if cache has expired.
-            \core_php_time_limit::raise(60);
-
-            $feed = new \moodle_simplepie();
-            // Set timeout for longer than normal to be agressive at
-            // fetching feeds if possible..
-            $feed->set_timeout(40);
-            $feed->set_cache_duration(0);
-            $feed->set_feed_url($rec->url);
-            $feed->init();
+            $feed = $this->fetch_feed($rec->url);
 
             if ($feed->error()) {
                 // Skip this feed (for an ever-increasing time if it keeps failing).
-                $block = new \block_rss_client();
-                $rec->skiptime = $block->calculate_skiptime($rec->skiptime);
+                $rec->skiptime = $this->calculate_skiptime($rec->skiptime);
                 $rec->skipuntil = time() + $rec->skiptime;
                 $DB->update_record('block_rss_client', $rec);
                 mtrace("Error: could not load/find the RSS feed - skipping for {$rec->skiptime} seconds.");
@@ -119,6 +111,41 @@ class refreshfeeds extends \core\task\scheduled_task {
 
         // Show times.
         mtrace($counter . ' feeds refreshed (took ' . microtime_diff($starttime, microtime()) . ' seconds)');
+    }
+
+    /**
+     * Fetch a feed for the specified URL.
+     *
+     * @param   string  $url The URL to fetch
+     * @return  \moodle_simplepie
+     */
+    protected function fetch_feed(string $url) : \moodle_simplepie {
+        // Fetch the rss feed, using standard simplepie caching so feeds will be renewed only if cache has expired.
+        \core_php_time_limit::raise(60);
+
+        $feed = new \moodle_simplepie();
+
+        // Set timeout for longer than normal to be agressive at fetching feeds if possible..
+        $feed->set_timeout(40);
+        $feed->set_cache_duration(0);
+        $feed->set_feed_url($url);
+        $feed->init();
+
+        return $feed;
+    }
+
+    /**
+     * Calculates a new skip time for a record based on the current skip time.
+     *
+     * @param   int     $currentskip The current skip time of a record.
+     * @return  int     The newly calculated skip time.
+     */
+    protected function calculate_skiptime(int $currentskip) : int {
+        // If the feed has never failed, then the initial skiptime will be 0. We use a default of 5 minutes in this case.
+        // If the feed has previously failed then we double that time.
+        $newskiptime = max(MINSECS * 5, ($currentskip * 2));
 
+        // Max out at the CLIENT_MAX_SKIPTIME.
+        return min($newskiptime, self::CLIENT_MAX_SKIPTIME);
     }
 }
index bae4cde..3b27084 100644 (file)
@@ -68,82 +68,74 @@ class block_rss_client_cron_testcase extends advanced_testcase {
         $this->assertContains('0 feeds refreshed (took ', $cronoutput);
     }
 
+    /**
+     * Data provider for skip time tests.
+     *
+     * @return  array
+     */
+    public function skip_time_increase_provider() : array {
+        return [
+            'Never failed' => [
+                'skiptime' => 0,
+                'skipuntil' => 0,
+                'newvalue' => MINSECS * 5,
+            ],
+            'Failed before' => [
+                // This should just double the time.
+                'skiptime' => 330,
+                'skipuntil' => time(),
+                'newvalue' => 660,
+            ],
+            'Near max' => [
+                'skiptime' => \block_rss_client\task\refreshfeeds::CLIENT_MAX_SKIPTIME - 5,
+                'skipuntil' => time(),
+                'newvalue' => \block_rss_client\task\refreshfeeds::CLIENT_MAX_SKIPTIME,
+            ],
+        ];
+    }
+
     /**
      * Test that when a feed has an error the skip time is increased correctly.
+     *
+     * @dataProvider    skip_time_increase_provider
      */
-    public function test_error() {
+    public function test_error($skiptime, $skipuntil, $newvalue) {
         global $DB, $CFG;
         $this->resetAfterTest();
+
+        require_once("{$CFG->libdir}/simplepie/moodle_simplepie.php");
+
         $time = time();
         // A record that has failed before.
-        $record = (object) array(
+        $record = (object) [
             'userid' => 1,
             'title' => 'Skip test feed',
             'preferredtitle' => '',
             'description' => 'A feed to test the skip time.',
             'shared' => 0,
             'url' => 'http://example.com/rss',
-            'skiptime' => 330,
-            'skipuntil' => $time - 300,
-        );
+            'skiptime' => $skiptime,
+            'skipuntil' => $skipuntil,
+        ];
         $record->id = $DB->insert_record('block_rss_client', $record);
 
-        // A record that has not failed before.
-        $record2 = (object) array(
-            'userid' => 1,
-            'title' => 'Skip test feed',
-            'preferredtitle' => '',
-            'description' => 'A feed to test the skip time.',
-            'shared' => 0,
-            'url' => 'http://example.com/rss2',
-            'skiptime' => 0,
-            'skipuntil' => 0,
-        );
-        $record2->id = $DB->insert_record('block_rss_client', $record2);
+        // Run the scheduled task and have it fail.
+        $task = $this->getMockBuilder(\block_rss_client\task\refreshfeeds::class)
+            ->setMethods(['fetch_feed'])
+            ->getMock();
 
-        // A record that is near the maximum wait time.
-        $record3 = (object) array(
-            'userid' => 1,
-            'title' => 'Skip test feed',
-            'preferredtitle' => '',
-            'description' => 'A feed to test the skip time.',
-            'shared' => 0,
-            'url' => 'http://example.com/rss3',
-            'skiptime' => block_rss_client::CLIENT_MAX_SKIPTIME - 5,
-            'skipuntil' => $time - 1,
-        );
-        $record3->id = $DB->insert_record('block_rss_client', $record3);
+        $piemock = $this->getMockBuilder(\moodle_simplepie::class)
+            ->setMethods(['error'])
+            ->getMock();
 
-        // Run the scheduled task.
-        $task = new \block_rss_client\task\refreshfeeds();
-        ob_start();
+        $piemock->method('error')
+            ->willReturn(true);
 
-        // Silence SimplePie php notices.
-        $errorlevel = error_reporting($CFG->debug & ~E_USER_NOTICE);
-        $task->execute();
-        error_reporting($errorlevel);
+        $task->method('fetch_feed')
+            ->willReturn($piemock);
 
-        $cronoutput = ob_get_clean();
-        $skiptime1 = $record->skiptime * 2;
-        $message1 = 'http://example.com/rss Error: could not load/find the RSS feed - skipping for ' . $skiptime1 . ' seconds.';
-        $this->assertContains($message1, $cronoutput);
-        $skiptime2 = 0;
-        $message2 = 'http://example.com/rss2 Error: could not load/find the RSS feed - skipping for ' . $skiptime2 . ' seconds.';
-        $this->assertContains($message2, $cronoutput);
-        $skiptime3 = block_rss_client::CLIENT_MAX_SKIPTIME;
-        $message3 = 'http://example.com/rss3 Error: could not load/find the RSS feed - skipping for ' . $skiptime3 . ' seconds.';
-        $this->assertContains($message3, $cronoutput);
-        $this->assertContains('0 feeds refreshed (took ', $cronoutput);
-
-        // Test that the records have been correctly updated.
-        $newrecord = $DB->get_record('block_rss_client', array('id' => $record->id));
-        $this->assertAttributeEquals($skiptime1, 'skiptime', $newrecord);
-        $this->assertAttributeGreaterThanOrEqual($time + $skiptime1, 'skipuntil', $newrecord);
-        $newrecord2 = $DB->get_record('block_rss_client', array('id' => $record2->id));
-        $this->assertAttributeEquals($skiptime2, 'skiptime', $newrecord2);
-        $this->assertAttributeGreaterThanOrEqual($time + $skiptime2, 'skipuntil', $newrecord2);
-        $newrecord3 = $DB->get_record('block_rss_client', array('id' => $record3->id));
-        $this->assertAttributeEquals($skiptime3, 'skiptime', $newrecord3);
-        $this->assertAttributeGreaterThanOrEqual($time + $skiptime3, 'skipuntil', $newrecord3);
+        // Run the cron and capture its output.
+        $this->expectOutputRegex("/.*Error: could not load\/find the RSS feed - skipping for {$newvalue} seconds.*/");
+        $task->execute();
     }
 }
index 2e53d97..b81c645 100644 (file)
@@ -135,6 +135,25 @@ function blog_remove_associations_for_course($courseid) {
     $DB->delete_records('blog_association', array('contextid' => $context->id));
 }
 
+/**
+ * Remove module associated blogs and blog tag instances.
+ *
+ * @param  int $modcontextid Module context ID.
+ */
+function blog_remove_associations_for_module($modcontextid) {
+    global $DB;
+
+    if (!empty($assocblogids = $DB->get_fieldset_select('blog_association', 'blogid',
+        'contextid = :contextid', ['contextid' => $modcontextid]))) {
+        list($sql, $params) = $DB->get_in_or_equal($assocblogids, SQL_PARAMS_NAMED);
+
+        $DB->delete_records_select('tag_instance', "itemid $sql", $params);
+        $DB->delete_records_select('post', "id $sql AND module = :module",
+            array_merge($params, ['module' => 'blog']));
+        $DB->delete_records('blog_association', ['contextid' => $modcontextid]);
+    }
+}
+
 /**
  * Given a record in the {blog_external} table, checks the blog's URL
  * for new entries not yet copied into Moodle.
index c2aa399..2776339 100644 (file)
@@ -240,6 +240,7 @@ class cache_helper {
         $invalidationeventset = false;
         $factory = cache_factory::instance();
         $inuse = $factory->get_caches_in_use();
+        $purgetoken = null;
         foreach ($instance->get_definitions() as $name => $definitionarr) {
             $definition = cache_definition::load($name, $definitionarr);
             if ($definition->invalidates_on_event($event)) {
@@ -266,8 +267,11 @@ class cache_helper {
                         $data = array();
                     }
                     // Add our keys to them with the current cache timestamp.
+                    if (null === $purgetoken) {
+                        $purgetoken = cache::get_purge_token(true);
+                    }
                     foreach ($keys as $key) {
-                        $data[$key] = cache::now();
+                        $data[$key] = $purgetoken;
                     }
                     // Set that data back to the cache.
                     $cache->set($event, $data);
@@ -315,6 +319,7 @@ class cache_helper {
         $invalidationeventset = false;
         $factory = cache_factory::instance();
         $inuse = $factory->get_caches_in_use();
+        $purgetoken = null;
         foreach ($instance->get_definitions() as $name => $definitionarr) {
             $definition = cache_definition::load($name, $definitionarr);
             if ($definition->invalidates_on_event($event)) {
@@ -338,8 +343,11 @@ class cache_helper {
                     // Get the event invalidation cache.
                     $cache = cache::make('core', 'eventinvalidation');
                     // Create a key to invalidate all.
+                    if (null === $purgetoken) {
+                        $purgetoken = cache::get_purge_token(true);
+                    }
                     $data = array(
-                        'purged' => cache::now()
+                        'purged' => $purgetoken,
                     );
                     // Set that data back to the cache.
                     $cache->set($event, $data);
index 168124d..691d2fb 100644 (file)
@@ -50,6 +50,14 @@ class cache implements cache_loader {
      */
     protected static $now;
 
+    /**
+     * A purge token used to distinguish between multiple cache purges in the same second.
+     * This is in the format <microtime>-<random string>.
+     *
+     * @var string
+     */
+    protected static $purgetoken;
+
     /**
      * The definition used when loading this cache if there was one.
      * @var cache_definition
@@ -286,33 +294,58 @@ class cache implements cache_loader {
             return;
         }
 
+        // Each cache stores the current 'lastinvalidation' value within the cache itself.
         $lastinvalidation = $this->get('lastinvalidation');
         if ($lastinvalidation === false) {
-            // This is a new cache or purged globally, there won't be anything to invalidate.
-            // Set the time of the last invalidation and move on.
-            $this->set('lastinvalidation', self::now());
+            // There is currently no  value for the lastinvalidation token, therefore the token is not set, and there
+            // can be nothing to invalidate.
+            // Set the lastinvalidation value to the current purge token and return early.
+            $this->set('lastinvalidation', self::get_purge_token());
             return;
-        } else if ($lastinvalidation == self::now()) {
-            // We've already invalidated during this request.
+        } else if ($lastinvalidation == self::get_purge_token()) {
+            // The current purge request has already been fully handled by this cache.
             return;
         }
 
-        // Get the event invalidation cache.
+        /*
+         * Now that the whole cache check is complete, we check the meaning of any specific cache invalidation events.
+         * These are stored in the core/eventinvalidation cache as an multi-dimensinoal array in the form:
+         *  [
+         *      eventname => [
+         *          keyname => purgetoken,
+         *      ]
+         *  ]
+         *
+         * The 'keyname' value is used to delete a specific key in the cache.
+         * If the keyname is set to the special value 'purged', then the whole cache is purged instead.
+         *
+         * The 'purgetoken' is the token that this key was last purged.
+         * a) If the purgetoken matches the last invalidation, then the key/cache is not purged.
+         * b) If the purgetoken is newer than the last invalidation, then the key/cache is not purged.
+         * c) If the purge token is older than the last invalidation, or it has a different token component, then the
+         *    cache is purged.
+         *
+         * Option b should not happen under normal operation, but may happen in race condition whereby a long-running
+         * request's cache is cleared in another process during that request, and prior to that long-running request
+         * creating the cache. In such a condition, it would be incorrect to clear that cache.
+         */
         $cache = self::make('core', 'eventinvalidation');
         $events = $cache->get_many($this->definition->get_invalidation_events());
         $todelete = array();
         $purgeall = false;
+
         // Iterate the returned data for the events.
         foreach ($events as $event => $keys) {
             if ($keys === false) {
                 // No data to be invalidated yet.
                 continue;
             }
+
             // Look at each key and check the timestamp.
-            foreach ($keys as $key => $timestamp) {
+            foreach ($keys as $key => $purgetoken) {
                 // If the timestamp of the event is more than or equal to the last invalidation (happened between the last
-                // invalidation and now)then we need to invaliate the key.
-                if ($timestamp >= $lastinvalidation) {
+                // invalidation and now)then we need to invaliate the key.
+                if (self::compare_purge_tokens($purgetoken, $lastinvalidation) > 0) {
                     if ($key === 'purged') {
                         $purgeall = true;
                         break;
@@ -330,7 +363,7 @@ class cache implements cache_loader {
         }
         // Set the time of the last invalidation.
         if ($purgeall || !empty($todelete)) {
-            $this->set('lastinvalidation', self::now());
+            $this->set('lastinvalidation', self::get_purge_token(true));
         }
     }
 
@@ -1186,13 +1219,70 @@ class cache implements cache_loader {
      * This stamp needs to be used for all ttl and time based operations to ensure that we don't end up with
      * timing issues.
      *
-     * @return int
+     * @param   bool    $float Whether to use floating precision accuracy.
+     * @return  int|float
      */
-    public static function now() {
+    public static function now($float = false) {
         if (self::$now === null) {
-            self::$now = time();
+            self::$now = microtime(true);
+        }
+
+        if ($float) {
+            return self::$now;
+        } else {
+            return (int) self::$now;
+        }
+    }
+
+    /**
+     * Get a 'purge' token used for cache invalidation handling.
+     *
+     * Note: This function is intended for use from within the Cache API only and not by plugins, or cache stores.
+     *
+     * @param   bool    $reset  Whether to reset the token and generate a new one.
+     * @return  string
+     */
+    public static function get_purge_token($reset = false) {
+        if (self::$purgetoken === null || $reset) {
+            self::$now = null;
+            self::$purgetoken = self::now(true) . '-' . uniqid('', true);
+        }
+
+        return self::$purgetoken;
+    }
+
+    /**
+     * Compare a pair of purge tokens.
+     *
+     * If the two tokens are identical, then the return value is 0.
+     * If the time component of token A is newer than token B, then a positive value is returned.
+     * If the time component of token B is newer than token A, then a negative value is returned.
+     *
+     * Note: This function is intended for use from within the Cache API only and not by plugins, or cache stores.
+     *
+     * @param   string  $tokena
+     * @param   string  $tokenb
+     * @return  int
+     */
+    public static function compare_purge_tokens($tokena, $tokenb) {
+        if ($tokena === $tokenb) {
+            // There is an exact match.
+            return 0;
+        }
+
+        // The token for when the cache was last invalidated.
+        list($atime) = explode('-', "{$tokena}-", 2);
+
+        // The token for this cache.
+        list($btime) = explode('-', "{$tokenb}-", 2);
+
+        if ($atime >= $btime) {
+            // Token A is newer.
+            return 1;
+        } else {
+            // Token A is older.
+            return -1;
         }
-        return self::$now;
     }
 }
 
index 8d04cd1..6aa268f 100644 (file)
@@ -1003,7 +1003,7 @@ class core_cache_testcase extends advanced_testcase {
         $timefile = $CFG->dataroot."/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/las-cache/lastinvalidation-$hash.cache";
         // Make sure the file is correct.
         $this->assertTrue(file_exists($timefile));
-        $timecont = serialize(cache::now() - 60); // Back 60sec in the past to force it to re-invalidate.
+        $timecont = serialize(cache::now(true) - 60); // Back 60sec in the past to force it to re-invalidate.
         make_writable_directory(dirname($timefile));
         file_put_contents($timefile, $timecont);
         $this->assertTrue(file_exists($timefile));
@@ -1029,6 +1029,7 @@ class core_cache_testcase extends advanced_testcase {
 
         // Test 2: Rebuild and test the invalidation of the event via the invalidation cache.
         cache_factory::reset();
+
         $instance = cache_config_testing::instance();
         $instance->phpunit_add_definition('phpunit/eventinvalidationtest', array(
             'mode' => cache_store::MODE_APPLICATION,
@@ -1040,6 +1041,7 @@ class core_cache_testcase extends advanced_testcase {
                 'crazyevent'
             )
         ));
+
         $cache = cache::make('phpunit', 'eventinvalidationtest');
         $this->assertFalse($cache->get('testkey1'));
 
@@ -1047,23 +1049,30 @@ class core_cache_testcase extends advanced_testcase {
 
         // Make a new cache class.  This should should invalidate testkey2.
         $cache = cache::make('phpunit', 'eventinvalidationtest');
-        // Timestamp should have updated to cache::now().
-        $this->assertEquals(cache::now(), $cache->get('lastinvalidation'));
+
+        // Invalidation token should have been reset.
+        $this->assertEquals(cache::get_purge_token(), $cache->get('lastinvalidation'));
 
         // Set testkey2 data.
         $cache->set('testkey2', 'test data 2');
+
         // Backdate the event invalidation time by 30 seconds.
         $invalidationcache = cache::make('core', 'eventinvalidation');
         $invalidationcache->set('crazyevent', array('testkey2' => cache::now() - 30));
+
         // Lastinvalidation should already be cache::now().
-        $this->assertEquals(cache::now(), $cache->get('lastinvalidation'));
+        $this->assertEquals(cache::get_purge_token(), $cache->get('lastinvalidation'));
+
         // Set it to 15 seconds ago so that we know if it changes.
-        $cache->set('lastinvalidation', cache::now() - 15);
+        $pasttime = cache::now(true) - 15;
+        $cache->set('lastinvalidation', $pasttime);
+
         // Make a new cache class.  This should not invalidate anything.
         cache_factory::instance()->reset_cache_instances();
         $cache = cache::make('phpunit', 'eventinvalidationtest');
+
         // Lastinvalidation shouldn't change since it was already newer than invalidation event.
-        $this->assertEquals(cache::now() - 15, $cache->get('lastinvalidation'));
+        $this->assertEquals($pasttime, $cache->get('lastinvalidation'));
 
         // Now set the event invalidation to newer than the lastinvalidation time.
         $invalidationcache->set('crazyevent', array('testkey2' => cache::now() - 5));
@@ -1071,18 +1080,18 @@ class core_cache_testcase extends advanced_testcase {
         cache_factory::instance()->reset_cache_instances();
         $cache = cache::make('phpunit', 'eventinvalidationtest');
         // Lastinvalidation timestamp should have updated to cache::now().
-        $this->assertEquals(cache::now(), $cache->get('lastinvalidation'));
+        $this->assertEquals(cache::get_purge_token(), $cache->get('lastinvalidation'));
 
         // Now simulate a purge_by_event 5 seconds ago.
         $invalidationcache = cache::make('core', 'eventinvalidation');
-        $invalidationcache->set('crazyevent', array('purged' => cache::now() - 5));
+        $invalidationcache->set('crazyevent', array('purged' => cache::now(true) - 5));
         // Set our lastinvalidation timestamp to 15 seconds ago.
-        $cache->set('lastinvalidation', cache::now() - 15);
+        $cache->set('lastinvalidation', cache::now(true) - 15);
         // Make a new cache class.  This should invalidate the cache.
         cache_factory::instance()->reset_cache_instances();
         $cache = cache::make('phpunit', 'eventinvalidationtest');
         // Lastinvalidation timestamp should have updated to cache::now().
-        $this->assertEquals(cache::now(), $cache->get('lastinvalidation'));
+        $this->assertEquals(cache::get_purge_token(), $cache->get('lastinvalidation'));
 
     }
 
@@ -2272,4 +2281,50 @@ class core_cache_testcase extends advanced_testcase {
         $this->assertArrayNotHasKey($sessionid, $endstats);
         $this->assertArrayNotHasKey($requestid, $endstats);
     }
+
+    /**
+     * Tests session cache event purge and subsequent visit in the same request.
+     *
+     * This test simulates a cache being created, a value being set, then the value being purged.
+     * A subsequent use of the same cache is started in the same request which fills the cache.
+     * A new request is started a short time later.
+     * The cache should be filled.
+     */
+    public function test_session_event_purge_same_second() {
+        $instance = cache_config_testing::instance();
+        $instance->phpunit_add_definition('phpunit/eventpurgetest', array(
+            'mode' => cache_store::MODE_SESSION,
+            'component' => 'phpunit',
+            'area' => 'eventpurgetest',
+            'invalidationevents' => array(
+                'crazyevent',
+            )
+        ));
+
+        // Create the cache, set a value, and immediately purge it by event.
+        $cache = cache::make('phpunit', 'eventpurgetest');
+        $cache->set('testkey1', 'test data 1');
+        $this->assertEquals('test data 1', $cache->get('testkey1'));
+        cache_helper::purge_by_event('crazyevent');
+        $this->assertFalse($cache->get('testkey1'));
+
+        // Set up the cache again in the same request and add a new value back in.
+        $factory = \cache_factory::instance();
+        $factory->reset_cache_instances();
+        $cache = cache::make('phpunit', 'eventpurgetest');
+        $cache->set('testkey1', 'test data 2');
+        $this->assertEquals('test data 2', $cache->get('testkey1'));
+
+        // Trick the cache into thinking that this is a new request.
+        cache_phpunit_cache::simulate_new_request();
+        $factory = \cache_factory::instance();
+        $factory->reset_cache_instances();
+
+        // Set up the cache again.
+        // This is a subsequent request at a new time, so we instead the invalidation time will be checked.
+        // The invalidation time should match the last purged time and the cache will not be re-purged.
+        $cache = cache::make('phpunit', 'eventpurgetest');
+        $this->assertEquals('test data 2', $cache->get('testkey1'));
+    }
+
 }
index 597c00a..6c42c39 100644 (file)
@@ -534,4 +534,21 @@ class cache_phpunit_factory extends cache_factory {
     public static function phpunit_disable() {
         parent::disable();
     }
-}
\ No newline at end of file
+}
+
+/**
+ * Cache PHPUnit specific Cache helper.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cache_phpunit_cache extends cache {
+    /**
+     * Make the changes which simulate a new request within the cache.
+     * This essentially resets currently held static values in the class, and increments the current timestamp.
+     */
+    public static function simulate_new_request() {
+        self::$now += 0.1;
+        self::$purgetoken = null;
+    }
+}
index 16bbbf5..8c3e287 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in /cache/stores/* - cache store plugins.
 Information provided here is intended especially for developers.
 
+=== 3.6 ===
+* The `cache::now()` function now takes an optional boolean parameter to indicate that the cache should return a more
+  accurate time, generated by the PHP `microtime` function.
+
 === 3.3 ===
 * Identifiers and invalidation events have been explictly been marked as incompatible and will
   throw a coding exception. Unexpected results would have occurred if the previous behaviour was attempted.
index 0afc594..4b7a850 100644 (file)
@@ -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;
index 6c42d3c..121a113 100644 (file)
@@ -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.
  *
index 807ecad..ff697dd 100644 (file)
@@ -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.
  *
index f848687..b3d5826 100644 (file)
@@ -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;
             }
 
index c585f3a..8b0d456 100644 (file)
@@ -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;
     }
index 80e60cd..a7d7840 100644 (file)
@@ -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,
index c3c7aac..3d8be58 100644 (file)
@@ -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;
     }
 
index 87289a6..3bf03df 100644 (file)
@@ -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;
     }
index 3b32555..dccdda4 100644 (file)
@@ -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);
index c00fe47..3466629 100644 (file)
@@ -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());
     }
 }
index 83df3a7..7dbe18b 100644 (file)
@@ -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)) {
index 09d17dc..a76246e 100644 (file)
@@ -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) {
index dddb26e..0b7ee24 100644 (file)
@@ -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)) {
index d6f79c3..7df273b 100644 (file)
@@ -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 (file)
index 0000000..a0345e6
--- /dev/null
@@ -0,0 +1,2853 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 <select> 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 core_course_category
+     */
+    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 self($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.
+     *
+     * @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 core_course_category object for this category.
+     *
+     * @return core_course_category
+     */
+    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;
+    }
+}
index 0d171c4..89fc598 100644 (file)
@@ -26,7 +26,6 @@ defined('MOODLE_INTERNAL') || die;
 
 require_once($CFG->libdir . '/formslib.php');
 require_once($CFG->libdir . '/questionlib.php');
-require_once($CFG->libdir . '/coursecatlib.php');
 
 /**
  * Delete category moodleform.
@@ -37,8 +36,8 @@ require_once($CFG->libdir . '/coursecatlib.php');
 class core_course_deletecategory_form extends moodleform {
 
     /**
-     * The coursecat object for that category being deleted.
-     * @var coursecat
+     * The core_course_category object for that category being deleted.
+     * @var core_course_category
      */
     protected $coursecat;
 
index c866f6c..2e58af3 100644 (file)
@@ -25,7 +25,6 @@
 defined('MOODLE_INTERNAL') || die;
 
 require_once($CFG->libdir.'/formslib.php');
-require_once($CFG->libdir.'/coursecatlib.php');
 
 /**
  * Edit category form.
@@ -52,7 +51,7 @@ class core_course_editcategory_form extends moodleform {
         }
         if ($categoryid) {
             // Editing an existing category.
-            $options += coursecat::make_categories_list('moodle/category:manage', $categoryid);
+            $options += core_course_category::make_categories_list('moodle/category:manage', $categoryid);
             if (empty($options[$parent])) {
                 // Ensure the the category parent has been included in the options.
                 $options[$parent] = $DB->get_field('course_categories', 'name', array('id'=>$parent));
@@ -60,7 +59,7 @@ class core_course_editcategory_form extends moodleform {
             $strsubmit = get_string('savechanges');
         } else {
             // Making a new category.
-            $options += coursecat::make_categories_list('moodle/category:manage');
+            $options += core_course_category::make_categories_list('moodle/category:manage');
             $strsubmit = get_string('createcategory');
         }
 
diff --git a/course/classes/list_element.php b/course/classes/list_element.php
new file mode 100644 (file)
index 0000000..cb9defc
--- /dev/null
@@ -0,0 +1,442 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains class core_course_list_element
+ *
+ * @package    core
+ * @subpackage course
+ * @copyright  2018 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * 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 core_course_category::search_courses()}
+ * and
+ * {@link core_course_category::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 core_course_category::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 core_course_category::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 core_course_list_element 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);
+            core_course_category::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);
+                core_course_category::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
+     * {@link core_course_list_element::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
+     * {@link core_course_list_element::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
+     * {@link core_course_list_element::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
+     * {@link core_course_list_element::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
+     * {@link core_course_list_element::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
+     * {@link core_course_list_element::can_access()}
+     *
+     * @return bool
+     */
+    public function can_restore() {
+        return has_capability('moodle/restore:restorecourse', $this->get_context());
+    }
+}
index fb02c22..2374883 100644 (file)
@@ -52,10 +52,10 @@ class helper {
      * Returns course details in an array ready to be printed.
      *
      * @global \moodle_database $DB
-     * @param \course_in_list $course
+     * @param \core_course_list_element $course
      * @return array
      */
-    public static function get_course_detail_array(\course_in_list $course) {
+    public static function get_course_detail_array(\core_course_list_element $course) {
         global $DB;
 
         $canaccess = $course->can_access();
@@ -72,7 +72,7 @@ class helper {
             }
         }
 
-        $category = \coursecat::get($course->category);
+        $category = \core_course_category::get($course->category);
         $categoryurl = new \moodle_url('/course/management.php', array('categoryid' => $course->category));
         $categoryname = $category->get_formatted_name();
 
@@ -164,10 +164,10 @@ class helper {
     /**
      * Returns an array of actions that can be performed upon a category being shown in a list.
      *
-     * @param \coursecat $category
+     * @param \core_course_category $category
      * @return array
      */
-    public static function get_category_listitem_actions(\coursecat $category) {
+    public static function get_category_listitem_actions(\core_course_category $category) {
         $manageurl = new \moodle_url('/course/management.php', array('categoryid' => $category->id));
         $baseurl = new \moodle_url($manageurl, array('sesskey' => \sesskey()));
         $actions = array();
@@ -313,11 +313,11 @@ class helper {
     /**
      * Returns an array of actions for a course listitem.
      *
-     * @param \coursecat $category
-     * @param \course_in_list $course
+     * @param \core_course_category $category
+     * @param \core_course_list_element $course
      * @return string
      */
-    public static function get_course_listitem_actions(\coursecat $category, \course_in_list $course) {
+    public static function get_course_listitem_actions(\core_course_category $category, \core_course_list_element $course) {
         $baseurl = new \moodle_url(
             '/course/management.php',
             array('courseid' => $course->id, 'categoryid' => $course->category, 'sesskey' => \sesskey())
@@ -371,10 +371,10 @@ class helper {
     /**
      * Returns an array of actions that can be performed on the course being displayed.
      *
-     * @param \course_in_list $course
+     * @param \core_course_list_element $course
      * @return array
      */
-    public static function get_course_detail_actions(\course_in_list $course) {
+    public static function get_course_detail_actions(\core_course_list_element $course) {
         $params = array('courseid' => $course->id, 'categoryid' => $course->category, 'sesskey' => \sesskey());
         $baseurl = new \moodle_url('/course/management.php', $params);
         $actions = array();
@@ -440,14 +440,15 @@ class helper {
     /**
      * Resorts the courses within a category moving the given course up by one.
      *
-     * @param \course_in_list $course
-     * @param \coursecat $category
+     * @param \core_course_list_element $course
+     * @param \core_course_category $category
      * @return bool
      * @throws \moodle_exception
      */
-    public static function action_course_change_sortorder_up_one(\course_in_list $course, \coursecat $category) {
+    public static function action_course_change_sortorder_up_one(\core_course_list_element $course,
+                                                                 \core_course_category $category) {
         if (!$category->can_resort_courses()) {
-            throw new \moodle_exception('permissiondenied', 'error', '', null, 'coursecat::can_resort');
+            throw new \moodle_exception('permissiondenied', 'error', '', null, 'core_course_category::can_resort');
         }
         return \course_change_sortorder_by_one($course, true);
     }
@@ -455,14 +456,15 @@ class helper {
     /**
      * Resorts the courses within a category moving the given course down by one.
      *
-     * @param \course_in_list $course
-     * @param \coursecat $category
+     * @param \core_course_list_element $course
+     * @param \core_course_category $category
      * @return bool
      * @throws \moodle_exception
      */
-    public static function action_course_change_sortorder_down_one(\course_in_list $course, \coursecat $category) {
+    public static function action_course_change_sortorder_down_one(\core_course_list_element $course,
+                                                                   \core_course_category $category) {
         if (!$category->can_resort_courses()) {
-            throw new \moodle_exception('permissiondenied', 'error', '', null, 'coursecat::can_resort');
+            throw new \moodle_exception('permissiondenied', 'error', '', null, 'core_course_category::can_resort');
         }
         return \course_change_sortorder_by_one($course, false);
     }
@@ -478,8 +480,8 @@ class helper {
         if (is_int($courserecordorid)) {
             $courserecordorid = get_course($courserecordorid);
         }
-        $course = new \course_in_list($courserecordorid);
-        $category = \coursecat::get($course->category);
+        $course = new \core_course_list_element($courserecordorid);
+        $category = \core_course_category::get($course->category);
         return self::action_course_change_sortorder_up_one($course, $category);
     }
 
@@ -494,8 +496,8 @@ class helper {
         if (is_int($courserecordorid)) {
             $courserecordorid = get_course($courserecordorid);
         }
-        $course = new \course_in_list($courserecordorid);
-        $category = \coursecat::get($course->category);
+        $course = new \core_course_list_element($courserecordorid);
+        $category = \core_course_category::get($course->category);
         return self::action_course_change_sortorder_down_one($course, $category);
     }
 
@@ -509,7 +511,7 @@ class helper {
      */
     public static function action_course_change_sortorder_after_course($courserecordorid, $moveaftercourseid) {
         $course = \get_course($courserecordorid);
-        $category = \coursecat::get($course->category);
+        $category = \core_course_category::get($course->category);
         if (!$category->can_resort_courses()) {
             $url = '/course/management.php?categoryid='.$course->category;
             throw new \moodle_exception('nopermissions', 'error', $url, \get_string('resortcourses', 'moodle'));
@@ -518,29 +520,31 @@ class helper {
     }
 
     /**
-     * Makes a course visible given a \course_in_list object.
+     * Makes a course visible given a \core_course_list_element object.
      *
-     * @param \course_in_list $course
+     * @param \core_course_list_element $course
      * @return bool
      * @throws \moodle_exception
      */
-    public static function action_course_show(\course_in_list $course) {
+    public static function action_course_show(\core_course_list_element $course) {
         if (!$course->can_change_visibility()) {
-            throw new \moodle_exception('permissiondenied', 'error', '', null, 'course_in_list::can_change_visbility');
+            throw new \moodle_exception('permissiondenied', 'error', '', null,
+                'core_course_list_element::can_change_visbility');
         }
         return course_change_visibility($course->id, true);
     }
 
     /**
-     * Makes a course hidden given a \course_in_list object.
+     * Makes a course hidden given a \core_course_list_element object.
      *
-     * @param \course_in_list $course
+     * @param \core_course_list_element $course
      * @return bool
      * @throws \moodle_exception
      */
-    public static function action_course_hide(\course_in_list $course) {
+    public static function action_course_hide(\core_course_list_element $course) {
         if (!$course->can_change_visibility()) {
-            throw new \moodle_exception('permissiondenied', 'error', '', null, 'course_in_list::can_change_visbility');
+            throw new \moodle_exception('permissiondenied', 'error', '', null,
+                'core_course_list_element::can_change_visbility');
         }
         return course_change_visibility($course->id, false);
     }
@@ -556,7 +560,7 @@ class helper {
         if (is_int($courserecordorid)) {
             $courserecordorid = get_course($courserecordorid);
         }
-        $course = new \course_in_list($courserecordorid);
+        $course = new \core_course_list_element($courserecordorid);
         return self::action_course_show($course);
     }
 
@@ -571,20 +575,20 @@ class helper {
         if (is_int($courserecordorid)) {
             $courserecordorid = get_course($courserecordorid);
         }
-        $course = new \course_in_list($courserecordorid);
+        $course = new \core_course_list_element($courserecordorid);
         return self::action_course_hide($course);
     }
 
     /**
      * Resort a categories subcategories shifting the given category up one.
      *
-     * @param \coursecat $category
+     * @param \core_course_category $category
      * @return bool
      * @throws \moodle_exception
      */
-    public static function action_category_change_sortorder_up_one(\coursecat $category) {
+    public static function action_category_change_sortorder_up_one(\core_course_category $category) {
         if (!$category->can_change_sortorder()) {
-            throw new \moodle_exception('permissiondenied', 'error', '', null, 'coursecat::can_change_sortorder');
+            throw new \moodle_exception('permissiondenied', 'error', '', null, 'core_course_category::can_change_sortorder');
         }
         return $category->change_sortorder_by_one(true);
     }
@@ -592,13 +596,13 @@ class helper {
     /**
      * Resort a categories subcategories shifting the given category down one.
      *
-     * @param \coursecat $category
+     * @param \core_course_category $category
      * @return bool
      * @throws \moodle_exception
      */
-    public static function action_category_change_sortorder_down_one(\coursecat $category) {
+    public static function action_category_change_sortorder_down_one(\core_course_category $category) {
         if (!$category->can_change_sortorder()) {
-            throw new \moodle_exception('permissiondenied', 'error', '', null, 'coursecat::can_change_sortorder');
+            throw new \moodle_exception('permissiondenied', 'error', '', null, 'core_course_category::can_change_sortorder');
         }
         return $category->change_sortorder_by_one(false);
     }
@@ -610,7 +614,7 @@ class helper {
      * @return bool
      */
     public static function action_category_change_sortorder_up_one_by_id($categoryid) {
-        $category = \coursecat::get($categoryid);
+        $category = \core_course_category::get($categoryid);
         return self::action_category_change_sortorder_up_one($category);
     }
 
@@ -621,72 +625,72 @@ class helper {
      * @return bool
      */
     public static function action_category_change_sortorder_down_one_by_id($categoryid) {
-        $category = \coursecat::get($categoryid);
+        $category = \core_course_category::get($categoryid);
         return self::action_category_change_sortorder_down_one($category);
     }
 
     /**
-     * Makes a category hidden given a \coursecat record.
+     * Makes a category hidden given a core_course_category object.
      *
-     * @param \coursecat $category
+     * @param \core_course_category $category
      * @return bool
      * @throws \moodle_exception
      */
-    public static function action_category_hide(\coursecat $category) {
+    public static function action_category_hide(\core_course_category $category) {
         if (!$category->can_change_visibility()) {
-            throw new \moodle_exception('permissiondenied', 'error', '', null, 'coursecat::can_change_visbility');
+            throw new \moodle_exception('permissiondenied', 'error', '', null, 'core_course_category::can_change_visbility');
         }
         $category->hide();
         return true;
     }
 
     /**
-     * Makes a category visible given a \coursecat record.
+     * Makes a category visible given a core_course_category object.
      *
-     * @param \coursecat $category
+     * @param \core_course_category $category
      * @return bool
      * @throws \moodle_exception
      */
-    public static function action_category_show(\coursecat $category) {
+    public static function action_category_show(\core_course_category $category) {
         if (!$category->can_change_visibility()) {
-            throw new \moodle_exception('permissiondenied', 'error', '', null, 'coursecat::can_change_visbility');
+            throw new \moodle_exception('permissiondenied', 'error', '', null, 'core_course_category::can_change_visbility');
         }
         $category->show();
         return true;
     }
 
     /**
-     * Makes a category visible given a \coursecat id or database record.
+     * Makes a category visible given a course category id or database record.
      *
      * @param int|\stdClass $categoryid
      * @return bool
      */
     public static function action_category_show_by_id($categoryid) {
-        return self::action_category_show(\coursecat::get($categoryid));
+        return self::action_category_show(\core_course_category::get($categoryid));
     }
 
     /**
-     * Makes a category hidden given a \coursecat id or database record.
+     * Makes a category hidden given a course category id or database record.
      *
      * @param int|\stdClass $categoryid
      * @return bool
      */
     public static function action_category_hide_by_id($categoryid) {
-        return self::action_category_hide(\coursecat::get($categoryid));
+        return self::action_category_hide(\core_course_category::get($categoryid));
     }
 
     /**
      * Resorts the sub categories of the given category.
      *
-     * @param \coursecat $category
+     * @param \core_course_category $category
      * @param string $sort One of idnumber or name.
      * @param bool $cleanup If true cleanup will be done, if false you will need to do it manually later.
      * @return bool
      * @throws \moodle_exception
      */
-    public static function action_category_resort_subcategories(\coursecat $category, $sort, $cleanup = true) {
+    public static function action_category_resort_subcategories(\core_course_category $category, $sort, $cleanup = true) {
         if (!$category->can_resort_subcategories()) {
-            throw new \moodle_exception('permissiondenied', 'error', '', null, 'coursecat::can_resort');
+            throw new \moodle_exception('permissiondenied', 'error', '', null, 'core_course_category::can_resort');
         }
         return $category->resort_subcategories($sort, $cleanup);
     }
@@ -694,15 +698,15 @@ class helper {
     /**
      * Resorts the courses within the given category.
      *
-     * @param \coursecat $category
+     * @param \core_course_category $category
      * @param string $sort One of fullname, shortname or idnumber
      * @param bool $cleanup If true cleanup will be done, if false you will need to do it manually later.
      * @return bool
      * @throws \moodle_exception
      */
-    public static function action_category_resort_courses(\coursecat $category, $sort, $cleanup = true) {
+    public static function action_category_resort_courses(\core_course_category $category, $sort, $cleanup = true) {
         if (!$category->can_resort_courses()) {
-            throw new \moodle_exception('permissiondenied', 'error', '', null, 'coursecat::can_resort');
+            throw new \moodle_exception('permissiondenied', 'error', '', null, 'core_course_category::can_resort');
         }
         return $category->resort_courses($sort, $cleanup);
     }
@@ -710,13 +714,14 @@ class helper {
     /**
      * Moves courses out of one category and into a new category.
      *
-     * @param \coursecat $oldcategory The category we are moving courses out of.
-     * @param \coursecat $newcategory The category we are moving courses into.
+     * @param \core_course_category $oldcategory The category we are moving courses out of.
+     * @param \core_course_category $newcategory The category we are moving courses into.
      * @param array $courseids The ID's of the courses we want to move.
      * @return bool True on success.
      * @throws \moodle_exception
      */
-    public static function action_category_move_courses_into(\coursecat $oldcategory, \coursecat $newcategory, array $courseids) {
+    public static function action_category_move_courses_into(\core_course_category $oldcategory,
+                                                             \core_course_category $newcategory, array $courseids) {
         global $DB;
 
         list($where, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
@@ -773,13 +778,13 @@ class helper {
             $searchcriteria = array('modulelist' => $modulelist);
         }
 
-        $courses = \coursecat::get(0)->search_courses($searchcriteria, array(
+        $courses = \core_course_category::get(0)->search_courses($searchcriteria, array(
             'recursive' => true,
             'offset' => $page * $perpage,
             'limit' => $perpage,
             'sort' => array('fullname' => 1)
         ));
-        $totalcount = \coursecat::get(0)->search_courses_count($searchcriteria, array('recursive' => true));
+        $totalcount = \core_course_category::get(0)->search_courses_count($searchcriteria, array('recursive' => true));
 
         return array($courses, \count($courses), $totalcount);
     }
@@ -790,7 +795,7 @@ class helper {
      * This function works much the same way as action_category_move_courses_into however it allows courses from multiple
      * categories to be moved into a single category.
      *
-     * @param int|\coursecat $categoryorid The category to move them into.
+     * @param int|\core_course_category $categoryorid The category to move them into.
      * @param array|int $courseids An array of course id's or optionally just a single course id.
      * @return bool True on success or false on failure.
      * @throws \moodle_exception
@@ -805,10 +810,10 @@ class helper {
         if (count($courseids) === 0) {
             return false;
         }
-        if ($categoryorid instanceof \coursecat) {
+        if ($categoryorid instanceof \core_course_category) {
             $moveto = $categoryorid;
         } else {
-            $moveto = \coursecat::get($categoryorid);
+            $moveto = \core_course_category::get($categoryorid);
         }
         if (!$moveto->can_move_courses_out_of() || !$moveto->can_move_courses_into()) {
             throw new \moodle_exception('cannotmovecourses');
@@ -824,7 +829,7 @@ class helper {
             }
             $catid = $courses[$id]->category;
             if (!isset($checks[$catid])) {
-                $coursecat = \coursecat::get($catid);
+                $coursecat = \core_course_category::get($catid);
                 $checks[$catid] = $coursecat->can_move_courses_out_of() && $coursecat->can_move_courses_into();
             }
             if (!$checks[$catid]) {
@@ -855,7 +860,7 @@ class helper {
      */
     public static function get_category_children_visibility($categoryid) {
         global $DB;
-        $category = \coursecat::get($categoryid);
+        $category = \core_course_category::get($categoryid);
         $select = $DB->sql_like('path', ':path');
         $path = $category->path . '/%';
 
@@ -869,10 +874,10 @@ class helper {
     /**
      * Records when a category is expanded or collapsed so that when the user
      *
-     * @param \coursecat $coursecat The category we're working with.
+     * @param \core_course_category $coursecat The category we're working with.
      * @param bool $expanded True if the category is expanded now.
      */
-    public static function record_expanded_category(\coursecat $coursecat, $expanded = true) {
+    public static function record_expanded_category(\core_course_category $coursecat, $expanded = true) {
         // If this ever changes we are going to reset it and reload the categories as required.
         self::$expandedcategories = null;
         $categoryid = $coursecat->id;
@@ -920,7 +925,7 @@ class helper {
      * Returns the categories that should be expanded when displaying the interface.
      *
      * @param int|null $withpath If specified a path to require as the parent.
-     * @return \coursecat[] An array of Category ID's to expand.
+     * @return \core_course_category[] An array of Category ID's to expand.
      */
     public static function get_expanded_categories($withpath = null) {
         if (self::$expandedcategories === null) {
index e2a4192..300af2c 100644 (file)
@@ -78,7 +78,7 @@ class core_course_management_renderer extends plugin_renderer_base {
             $html .= html_writer::start_div();
             $html .= $this->view_mode_selector(\core_course\management\helper::get_management_viewmodes(), $viewmode);
             if ($viewmode === 'courses') {
-                $categories = coursecat::make_categories_list(array('moodle/category:manage', 'moodle/course:create'));
+                $categories = core_course_category::make_categories_list(array('moodle/category:manage', 'moodle/course:create'));
                 $nothing = false;
                 if ($categoryid === null) {
                     $nothing = array('' => get_string('selectacategory'));
@@ -119,10 +119,10 @@ class core_course_management_renderer extends plugin_renderer_base {
     /**
      * 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();
@@ -136,7 +136,7 @@ class core_course_management_renderer extends plugin_renderer_base {
         $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',
@@ -173,14 +173,14 @@ class core_course_management_renderer extends plugin_renderer_base {
      *
      * 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);
@@ -313,23 +313,23 @@ class core_course_management_renderer extends plugin_renderer_base {
      * 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'));
         }
-        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) {
@@ -341,11 +341,11 @@ class core_course_management_renderer extends plugin_renderer_base {
     /**
      * Renderers the actions for individual category list items.
      *
-     * @param coursecat $category
+     * @param core_course_category $category
      * @param array $actions
      * @return string
      */
-    public function category_listitem_actions(coursecat $category, array $actions = null) {
+    public function category_listitem_actions(core_course_category $category, array $actions = null) {
         if ($actions === null) {
             $actions = \core_course\management\helper::get_category_listitem_actions($category);
         }
@@ -377,20 +377,20 @@ class core_course_management_renderer extends plugin_renderer_base {
     /**
      * Renders bulk actions for categories.
      *
-     * @param coursecat $category The currently selected category if there is one.
+     * @param core_course_category $category The currently selected category if there is one.
      * @return string
      */
-    public function category_bulk_actions(coursecat $category = null) {
+    public function category_bulk_actions(core_course_category $category = null) {
         // Resort courses.
         // Change parent.
-        if (!coursecat::can_resort_any() && !coursecat::can_change_parent_any()) {
+        if (!core_course_category::can_resort_any() && !core_course_category::can_change_parent_any()) {
             return '';
         }
         $strgo = new lang_string('go');
 
         $html  = html_writer::start_div('category-bulk-actions bulk-actions');
         $html .= html_writer::div(get_string('categorybulkaction'), 'accesshide', array('tabindex' => '0'));
-        if (coursecat::can_resort_any()) {
+        if (core_course_category::can_resort_any()) {
             $selectoptions = array(
                 'selectedcategories' => get_string('selectedcategories'),
                 'allcategories' => get_string('allcategories')
@@ -452,12 +452,12 @@ class core_course_management_renderer extends plugin_renderer_base {
             $html .= html_writer::div($form, 'pair-value span9 col-md-9 yui3-u-3-4');
             $html .= html_writer::end_div();
         }
-        if (coursecat::can_change_parent_any()) {
+        if (core_course_category::can_change_parent_any()) {
             $options = array();
             if (has_capability('moodle/category:manage', context_system::instance())) {
-                $options[0] = coursecat::get(0)->get_formatted_name();
+                $options[0] = core_course_category::get(0)->get_formatted_name();
             }
-            $options += coursecat::make_categories_list('moodle/category:manage');
+            $options += core_course_category::make_categories_list('moodle/category:manage');
             $select = html_writer::select(
                 $options,
                 'movecategoriesto',
@@ -479,14 +479,15 @@ class core_course_management_renderer extends plugin_renderer_base {
     /**
      * 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, $page = 0, $perpage = 20,
+    public function course_listing(core_course_category $category = null, core_course_list_element $course = null,
+                                   $page = 0, $perpage = 20,
         $viewmode = 'default') {
 
         if ($category === null) {
@@ -543,14 +544,15 @@ class core_course_management_renderer extends plugin_renderer_base {
     /**
      * Renders pagination for a course listing.
      *
-     * @param coursecat $category The category to produce pagination for.
+     * @param core_course_category $category The category to produce pagination for.
      * @param int $page The current page.
      * @param int $perpage The number of courses to display per page.
      * @param bool $showtotals Set to true to show the total number of courses and what is being displayed.
      * @param string|null $viewmode The view mode the page is in, one out of 'default', 'combined', 'courses' or 'categories'.
      * @return string
      */
-    protected function listing_pagination(coursecat $category, $page, $perpage, $showtotals = false, $viewmode = 'default') {
+    protected function listing_pagination(core_course_category $category, $page, $perpage, $showtotals = false,
+                                          $viewmode = 'default') {
         $html = '';
         $totalcourses = $category->get_courses_count();
         $totalpages = ceil($totalcourses / $perpage);
@@ -618,12 +620,12 @@ class core_course_management_renderer extends plugin_renderer_base {
      *
      * 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(
@@ -676,12 +678,12 @@ class core_course_management_renderer extends plugin_renderer_base {
      *
      * 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'));
@@ -755,11 +757,11 @@ class core_course_management_renderer extends plugin_renderer_base {
     /**
      * Renderers actions for individual course actions.
      *
-     * @param coursecat $category The currently selected category.
-     * @param course_in_list $course The course to renderer actions for.
+     * @param core_course_category $category The currently selected category.
+     * @param core_course_list_element  $course The course to renderer actions for.
      * @return string
      */
-    public function course_listitem_actions(coursecat $category, course_in_list $course) {
+    public function course_listitem_actions(core_course_category $category, core_course_list_element $course) {
         $actions = \core_course\management\helper::get_course_listitem_actions($category, $course);
         if (empty($actions)) {
             return '';
@@ -775,15 +777,15 @@ class core_course_management_renderer extends plugin_renderer_base {
     /**
      * Renderers bulk actions that can be performed on courses.
      *
-     * @param coursecat $category The currently selected category and the category in which courses that
+     * @param core_course_category $category The currently selected category and the category in which courses that
      *      are selectable belong.
      * @return string
      */
-    public function course_bulk_actions(coursecat $category) {
+    public function course_bulk_actions(core_course_category $category) {
         $html  = html_writer::start_div('course-bulk-actions bulk-actions');
         if ($category->can_move_courses_out_of()) {
             $html .= html_writer::div(get_string('coursebulkaction'), 'accesshide', array('tabindex' => '0'));
-            $options = coursecat::make_categories_list('moodle/category:manage');
+            $options = core_course_category::make_categories_list('moodle/category:manage');
             $select = html_writer::select(
                 $options,
                 'movecoursesto',
@@ -810,7 +812,7 @@ class core_course_management_renderer extends plugin_renderer_base {
     public function course_search_bulk_actions() {
         $html  = html_writer::start_div('course-bulk-actions bulk-actions');
         $html .= html_writer::div(get_string('coursebulkaction'), 'accesshide', array('tabindex' => '0'));
-        $options = coursecat::make_categories_list('moodle/category:manage');
+        $options = core_course_category::make_categories_list('moodle/category:manage');
         $select = html_writer::select(
             $options,
             'movecoursesto',
@@ -831,10 +833,10 @@ class core_course_management_renderer extends plugin_renderer_base {
     /**
      * 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'];
 
@@ -867,10 +869,10 @@ class core_course_management_renderer extends plugin_renderer_base {
     /**
      * 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 '';
@@ -1089,13 +1091,13 @@ class core_course_management_renderer extends plugin_renderer_base {
      *
      * @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);
@@ -1203,11 +1205,11 @@ class core_course_management_renderer extends plugin_renderer_base {
      *
      * 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(
@@ -1217,7 +1219,7 @@ class core_course_management_renderer extends plugin_renderer_base {
             '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()) {