diff --git a/blocks/course_list/block_course_list.php b/blocks/course_list/block_course_list.php
index 540c49afc27..a9aeb45cd64 100644
--- a/blocks/course_list/block_course_list.php
+++ b/blocks/course_list/block_course_list.php
@@ -1,6 +1,7 @@
dirroot . '/course/lib.php');
+include_once($CFG->libdir . '/coursecatlib.php');
class block_course_list extends block_list {
function init() {
@@ -53,11 +54,11 @@ class block_course_list extends block_list {
}
}
- $categories = get_categories("0"); // Parent = 0 ie top-level categories only
+ $categories = coursecat::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) {
- $categoryname = format_string($category->name, true, array('context' => context_coursecat::instance($category->id)));
+ $categoryname = $category->get_formatted_name();
$linkcss = $category->visible ? "" : " class=\"dimmed\" ";
$this->content->items[]="wwwroot/course/category.php?id=$category->id\">".$icon . $categoryname . "";
}
diff --git a/cohort/edit_form.php b/cohort/edit_form.php
index c23c249b97c..fb7409e901e 100644
--- a/cohort/edit_form.php
+++ b/cohort/edit_form.php
@@ -85,9 +85,9 @@ class cohort_edit_form extends moodleform {
}
protected function get_category_options($currentcontextid) {
- $displaylist = array();
- $parentlist = array();
- make_categories_list($displaylist, $parentlist, 'moodle/cohort:manage');
+ global $CFG;
+ require_once($CFG->libdir. '/coursecatlib.php');
+ $displaylist = coursecat::make_categories_list('moodle/cohort:manage');
$options = array();
$syscontext = context_system::instance();
if (has_capability('moodle/cohort:manage', $syscontext)) {
diff --git a/cohort/lib.php b/cohort/lib.php
index b71b00b1b7e..19441a51749 100644
--- a/cohort/lib.php
+++ b/cohort/lib.php
@@ -102,7 +102,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 $category
+ * @param stdClass|coursecat $category
* @return void
*/
function cohort_delete_category($category) {
diff --git a/course/category.php b/course/category.php
index e32eba9afb0..6ed18982b22 100644
--- a/course/category.php
+++ b/course/category.php
@@ -26,6 +26,7 @@
require_once("../config.php");
require_once($CFG->dirroot.'/course/lib.php');
require_once($CFG->libdir.'/textlib.class.php');
+require_once($CFG->libdir. '/coursecatlib.php');
$id = required_param('id', PARAM_INT); // Category id
$page = optional_param('page', 0, PARAM_INT); // which page to show
@@ -69,9 +70,7 @@ $PAGE->set_button(print_course_search('', true, 'navbar'));
echo $OUTPUT->header();
/// Print the category selector
-$displaylist = array();
-$notused = array();
-make_categories_list($displaylist, $notused);
+$displaylist = coursecat::make_categories_list();
echo '
';
$select = new single_select(new moodle_url('/course/category.php'), 'id', $displaylist, $category->id, null, 'switchcategory');
diff --git a/course/completion_form.php b/course/completion_form.php
index 4344aa3da46..7dab70af6c6 100644
--- a/course/completion_form.php
+++ b/course/completion_form.php
@@ -97,9 +97,8 @@ class course_completion_form extends moodleform {
}
// Get category list
- $list = array();
- $parents = array();
- make_categories_list($list, $parents);
+ require_once($CFG->libdir. '/coursecatlib.php');
+ $list = coursecat::make_categories_list();
// Get course list for select box
$selectbox = array();
diff --git a/course/delete_category_form.php b/course/delete_category_form.php
index e7af4b1111d..f76655e18c1 100644
--- a/course/delete_category_form.php
+++ b/course/delete_category_form.php
@@ -6,69 +6,25 @@ if (!defined('MOODLE_INTERNAL')) {
require_once($CFG->libdir.'/formslib.php');
require_once($CFG->libdir.'/questionlib.php');
+require_once($CFG->libdir. '/coursecatlib.php');
class delete_category_form extends moodleform {
var $_category;
function definition() {
- global $CFG, $DB;
+ $mform = & $this->_form;
+ $this->_category = $this->_customdata;
+ $categorycontext = context_coursecat::instance($this->_category->id);
- $mform =& $this->_form;
- $category = $this->_customdata;
- $categorycontext = context_coursecat::instance($category->id);
- $this->_category = $category;
+ // Check permissions, to see if it OK to give the option to delete
+ // the contents, rather than move elsewhere.
+ $candeletecontent = $this->_category->can_delete_full();
- /// Check permissions, to see if it OK to give the option to delete
- /// the contents, rather than move elsewhere.
- /// Are there any subcategories of this one, can they be deleted?
- $candeletecontent = true;
- $tocheck = get_child_categories($category->id);
- $containscategories = !empty($tocheck);
- $categoryids = array($category->id);
- while (!empty($tocheck)) {
- $checkcat = array_pop($tocheck);
- $childcategoryids[] = $checkcat->id;
- $tocheck = $tocheck + get_child_categories($checkcat->id);
- $chcontext = context_coursecat::instance($checkcat->id);
- if ($candeletecontent && !has_capability('moodle/category:manage', $chcontext)) {
- $candeletecontent = false;
- }
- }
+ // Get the list of categories we might be able to move to.
+ $displaylist = $this->_category->move_content_targets_list();
- /// Are there any courses in here, can they be deleted?
- list($test, $params) = $DB->get_in_or_equal($categoryids);
- $containedcourses = $DB->get_records_sql(
- "SELECT id,1 FROM {course} c WHERE c.category $test", $params);
- $containscourses = false;
- if ($containedcourses) {
- $containscourses = true;
- foreach ($containedcourses as $courseid => $notused) {
- if ($candeletecontent && !can_delete_course($courseid)) {
- $candeletecontent = false;
- break;
- }
- }
- }
-
- /// Are there any questions in the question bank here?
- $containsquestions = question_context_has_any_questions($categorycontext);
-
- /// Get the list of categories we might be able to move to.
- $testcaps = array();
- if ($containscourses) {
- $testcaps[] = 'moodle/course:create';
- }
- if ($containscategories || $containsquestions) {
- $testcaps[] = 'moodle/category:manage';
- }
- $displaylist = array();
- $notused = array();
- if (!empty($testcaps)) {
- make_categories_list($displaylist, $notused, $testcaps, $category->id);
- }
-
- /// Now build the options.
+ // Now build the options.
$options = array();
if ($displaylist) {
$options[0] = get_string('movecontentstoanothercategory');
@@ -76,60 +32,57 @@ class delete_category_form extends moodleform {
if ($candeletecontent) {
$options[1] = get_string('deleteallcannotundo');
}
+ if (empty($options)) {
+ print_error('youcannotdeletecategory', 'error', 'index.php', $this->_category->get_formatted_name());
+ }
- /// Now build the form.
- $mform->addElement('header','general', get_string('categorycurrentcontents', '', format_string($category->name, true, array('context' => $categorycontext))));
+ // Now build the form.
+ $mform->addElement('header','general', get_string('categorycurrentcontents', '', $this->_category->get_formatted_name()));
- if ($containscourses || $containscategories || $containsquestions) {
- if (empty($options)) {
- print_error('youcannotdeletecategory', 'error', 'index.php', format_string($category->name, true, array('context' => $categorycontext)));
- }
-
- /// Describe the contents of this category.
- $contents = '
';
+ }
+ if (!empty($contents)) {
+ $mform->addElement('static', 'emptymessage', get_string('thiscategorycontains'), html_writer::tag('ul', $contents));
} else {
- $mform->addElement('hidden', 'fulldelete', 1);
- $mform->setType('fulldelete', PARAM_INT);
$mform->addElement('static', 'emptymessage', '', get_string('deletecategoryempty'));
}
+ // Give the options for what to do.
+ $mform->addElement('select', 'fulldelete', get_string('whattodo'), $options);
+ if (count($options) == 1) {
+ $optionkeys = array_keys($options);
+ $option = reset($optionkeys);
+ $mform->hardFreeze('fulldelete');
+ $mform->setConstant('fulldelete', $option);
+ }
+
+ if ($displaylist) {
+ $mform->addElement('select', 'newparent', get_string('movecategorycontentto'), $displaylist);
+ if (in_array($this->_category->parent, $displaylist)) {
+ $mform->setDefault('newparent', $this->_category->parent);
+ }
+ $mform->disabledIf('newparent', 'fulldelete', 'eq', '1');
+ }
+
$mform->addElement('hidden', 'deletecat');
$mform->setType('deletecat', PARAM_ALPHANUM);
$mform->addElement('hidden', 'sure');
$mform->setType('sure', PARAM_ALPHANUM);
- $mform->setDefault('sure', md5(serialize($category)));
+ $mform->setDefault('sure', md5(serialize($this->_category)));
//--------------------------------------------------------------------------------
$this->add_action_buttons(true, get_string('delete'));
+ $this->set_data(array('deletecat' => $this->_category->id));
}
/// perform some extra moodle validation
diff --git a/course/edit_form.php b/course/edit_form.php
index 625f8462993..bfa6ca6c2f6 100644
--- a/course/edit_form.php
+++ b/course/edit_form.php
@@ -4,6 +4,7 @@ defined('MOODLE_INTERNAL') || die;
require_once($CFG->libdir.'/formslib.php');
require_once($CFG->libdir.'/completionlib.php');
+require_once($CFG->libdir. '/coursecatlib.php');
class course_edit_form extends moodleform {
protected $course;
@@ -48,9 +49,7 @@ class course_edit_form extends moodleform {
// verify permissions to change course category or keep current
if (empty($course->id)) {
if (has_capability('moodle/course:create', $categorycontext)) {
- $displaylist = array();
- $parentlist = array();
- make_categories_list($displaylist, $parentlist, 'moodle/course:create');
+ $displaylist = coursecat::make_categories_list('moodle/course:create');
$mform->addElement('select', 'category', get_string('category'), $displaylist);
$mform->addHelpButton('category', 'category');
$mform->setDefault('category', $category->id);
@@ -61,12 +60,10 @@ class course_edit_form extends moodleform {
}
} else {
if (has_capability('moodle/course:changecategory', $coursecontext)) {
- $displaylist = array();
- $parentlist = array();
- make_categories_list($displaylist, $parentlist, 'moodle/course:create');
+ $displaylist = coursecat::make_categories_list('moodle/course:create');
if (!isset($displaylist[$course->category])) {
//always keep current
- $displaylist[$course->category] = format_string($DB->get_field('course_categories', 'name', array('id'=>$course->category)));
+ $displaylist[$course->category] = coursecat::get($course->category)->get_formatted_name();
}
$mform->addElement('select', 'category', get_string('category'), $displaylist);
$mform->addHelpButton('category', 'category');
diff --git a/course/editcategory.php b/course/editcategory.php
index b7184b7c09a..355aca195ee 100644
--- a/course/editcategory.php
+++ b/course/editcategory.php
@@ -27,8 +27,9 @@
*/
require_once('../config.php');
-require_once('lib.php');
-require_once('editcategory_form.php');
+require_once($CFG->dirroot.'/course/lib.php');
+require_once($CFG->dirroot.'/course/editcategory_form.php');
+require_once($CFG->libdir.'/coursecatlib.php');
require_login();
@@ -92,42 +93,16 @@ if ($mform->is_cancelled()) {
redirect($CFG->wwwroot .'/course/manage.php');
}
} else if ($data = $mform->get_data()) {
- $newcategory = new stdClass();
- $newcategory->name = $data->name;
- $newcategory->idnumber = $data->idnumber;
- $newcategory->description_editor = $data->description_editor;
- $newcategory->parent = $data->parent; // if $data->parent = 0, the new category will be a top-level category
-
- if (isset($data->theme) && !empty($CFG->allowcategorythemes)) {
- $newcategory->theme = $data->theme;
- }
-
- $logaction = 'update';
if ($id) {
- // Update an existing category.
- $newcategory->id = $category->id;
- if ($newcategory->parent != $category->parent) {
- // check category manage capability if parent changed
- require_capability('moodle/category:manage', get_category_or_system_context((int)$newcategory->parent));
- $parent_cat = $DB->get_record('course_categories', array('id' => $newcategory->parent));
- move_category($newcategory, $parent_cat);
+ $newcategory = coursecat::get($id);
+ if ($data->parent != $category->parent && !$newcategory->can_change_parent($data->parent)) {
+ print_error('cannotmovecategory');
}
+ $newcategory->update($data, $editoroptions);
} else {
- // Create a new category.
- $newcategory->description = $data->description_editor['text'];
-
- // Don't overwrite the $newcategory object as it'll be processed by file_postupdate_standard_editor in a moment
- $category = create_course_category($newcategory);
- $newcategory->id = $category->id;
- $categorycontext = $category->context;
- $logaction = 'add';
+ $newcategory = coursecat::create($data, $editoroptions);
}
- $newcategory = file_postupdate_standard_editor($newcategory, 'description', $editoroptions, $categorycontext, 'coursecat', 'description', 0);
- $DB->update_record('course_categories', $newcategory);
- add_to_log(SITEID, "category", $logaction, "editcategory.php?id=$newcategory->id", $newcategory->id);
- fix_course_sortorder();
-
redirect('manage.php?id='.$newcategory->id);
}
diff --git a/course/editcategory_form.php b/course/editcategory_form.php
index 3e3cfe32fad..40ac48c1fe1 100644
--- a/course/editcategory_form.php
+++ b/course/editcategory_form.php
@@ -4,6 +4,7 @@ if (!defined('MOODLE_INTERNAL')) {
}
require_once ($CFG->dirroot.'/course/moodleform_mod.php');
+require_once ($CFG->libdir.'/coursecatlib.php');
class editcategory_form extends moodleform {
// form definition
@@ -18,17 +19,16 @@ class editcategory_form extends moodleform {
if (has_capability('moodle/category:manage', get_system_context()) || $category->parent == 0) {
$options[0] = get_string('top');
}
- $parents = array();
if ($category->id) {
// Editing an existing category.
- make_categories_list($options, $parents, 'moodle/category:manage', $category->id);
+ $options += coursecat::make_categories_list('moodle/category:manage', $category->id);
if (empty($options[$category->parent])) {
$options[$category->parent] = $DB->get_field('course_categories', 'name', array('id'=>$category->parent));
}
$strsubmit = get_string('savechanges');
} else {
// Making a new category
- make_categories_list($options, $parents, 'moodle/category:manage');
+ $options += coursecat::make_categories_list('moodle/category:manage');
$strsubmit = get_string('createcategory');
}
diff --git a/course/externallib.php b/course/externallib.php
index 3063966d57f..362f9287f0c 100644
--- a/course/externallib.php
+++ b/course/externallib.php
@@ -1650,7 +1650,7 @@ class core_course_external extends external_api {
*/
public static function create_categories($categories) {
global $CFG, $DB;
- require_once($CFG->dirroot . "/course/lib.php");
+ require_once($CFG->libdir . "/coursecatlib.php");
$params = self::validate_parameters(self::create_categories_parameters(),
array('categories' => $categories));
@@ -1670,38 +1670,10 @@ class core_course_external extends external_api {
self::validate_context($context);
require_capability('moodle/category:manage', $context);
- // Check name.
- if (textlib::strlen($category['name'])>255) {
- throw new moodle_exception('categorytoolong');
- }
+ // this will validate format and throw an exception if there are errors
+ external_validate_format($category['descriptionformat']);
- $newcategory = new stdClass();
- $newcategory->name = $category['name'];
- $newcategory->parent = $category['parent'];
- // Format the description.
- if (!empty($category['description'])) {
- $newcategory->description = $category['description'];
- }
- $newcategory->descriptionformat = external_validate_format($category['descriptionformat']);
- if (isset($category['theme']) and !empty($CFG->allowcategorythemes)) {
- $newcategory->theme = $category['theme'];
- }
- // Check id number.
- if (!empty($category['idnumber'])) { // Same as in course/editcategory_form.php .
- if (textlib::strlen($category['idnumber'])>100) {
- throw new moodle_exception('idnumbertoolong');
- }
- if ($existing = $DB->get_record('course_categories', array('idnumber' => $category['idnumber']))) {
- if ($existing->id) {
- throw new moodle_exception('idnumbertaken');
- }
- }
- $newcategory->idnumber = $category['idnumber'];
- }
-
- $newcategory = create_course_category($newcategory);
- // Populate special fields.
- fix_course_sortorder();
+ $newcategory = coursecat::create($category);
$createdcategories[] = array('id' => $newcategory->id, 'name' => $newcategory->name);
}
@@ -1764,7 +1736,7 @@ class core_course_external extends external_api {
*/
public static function update_categories($categories) {
global $CFG, $DB;
- require_once($CFG->dirroot . "/course/lib.php");
+ require_once($CFG->libdir . "/coursecatlib.php");
// Validate parameters.
$params = self::validate_parameters(self::update_categories_parameters(), array('categories' => $categories));
@@ -1772,49 +1744,16 @@ class core_course_external extends external_api {
$transaction = $DB->start_delegated_transaction();
foreach ($params['categories'] as $cat) {
- if (!$category = $DB->get_record('course_categories', array('id' => $cat['id']))) {
- throw new moodle_exception('unknowcategory');
- }
+ $category = coursecat::get($cat['id']);
$categorycontext = context_coursecat::instance($cat['id']);
self::validate_context($categorycontext);
require_capability('moodle/category:manage', $categorycontext);
- if (!empty($cat['name'])) {
- if (textlib::strlen($cat['name'])>255) {
- throw new moodle_exception('categorytoolong');
- }
- $category->name = $cat['name'];
- }
- if (!empty($cat['idnumber'])) {
- if (textlib::strlen($cat['idnumber'])>100) {
- throw new moodle_exception('idnumbertoolong');
- }
- $category->idnumber = $cat['idnumber'];
- }
- if (!empty($cat['description'])) {
- $category->description = $cat['description'];
- $category->descriptionformat = external_validate_format($cat['descriptionformat']);
- }
- if (!empty($cat['theme'])) {
- $category->theme = $cat['theme'];
- }
- if (!empty($cat['parent']) && ($category->parent != $cat['parent'])) {
- // First check if parent exists.
- if (!$parent_cat = $DB->get_record('course_categories', array('id' => $cat['parent']))) {
- throw new moodle_exception('unknowcategory');
- }
- // Then check if we have capability.
- self::validate_context(get_category_or_system_context((int)$cat['parent']));
- require_capability('moodle/category:manage', get_category_or_system_context((int)$cat['parent']));
- // Finally move the category.
- move_category($category, $parent_cat);
- $category->parent = $cat['parent'];
- // Get updated path by move_category().
- $category->path = $DB->get_field('course_categories', 'path',
- array('id' => $category->id));
- }
- $DB->update_record('course_categories', $category);
+ // this will throw an exception if descriptionformat is not valid
+ external_validate_format($cat['descriptionformat']);
+
+ $category->update($cat);
}
$transaction->allow_commit();
@@ -1864,6 +1803,7 @@ class core_course_external extends external_api {
public static function delete_categories($categories) {
global $CFG, $DB;
require_once($CFG->dirroot . "/course/lib.php");
+ require_once($CFG->libdir . "/coursecatlib.php");
// Validate parameters.
$params = self::validate_parameters(self::delete_categories_parameters(), array('categories' => $categories));
@@ -1871,9 +1811,7 @@ class core_course_external extends external_api {
$transaction = $DB->start_delegated_transaction();
foreach ($params['categories'] as $category) {
- if (!$deletecat = $DB->get_record('course_categories', array('id' => $category['id']))) {
- throw new moodle_exception('unknowcategory');
- }
+ $deletecat = coursecat::get($category['id'], MUST_EXIST);
$context = context_coursecat::instance($deletecat->id);
require_capability('moodle/category:manage', $context);
self::validate_context($context);
@@ -1881,29 +1819,32 @@ class core_course_external extends external_api {
if ($category['recursive']) {
// If recursive was specified, then we recursively delete the category's contents.
- category_delete_full($deletecat, false);
+ if ($deletecat->can_delete_full()) {
+ $deletecat->delete_full(false);
+ } else {
+ throw new moodle_exception('youcannotdeletecategory', '', '', $deletecat->get_formatted_name());
+ }
} else {
// In this situation, we don't delete the category's contents, we either move it to newparent or parent.
// If the parent is the root, moving is not supported (because a course must always be inside a category).
// We must move to an existing category.
if (!empty($category['newparent'])) {
- if (!$DB->record_exists('course_categories', array('id' => $category['newparent']))) {
- throw new moodle_exception('unknowcategory');
- }
- $newparent = $category['newparent'];
+ $newparentcat = coursecat::get($category['newparent']);
} else {
- $newparent = $deletecat->parent;
+ $newparentcat = coursecat::get($deletecat->parent);
}
// This operation is not allowed. We must move contents to an existing category.
- if ($newparent == 0) {
+ if (!$newparentcat->id) {
throw new moodle_exception('movecatcontentstoroot');
}
- $parentcontext = get_category_or_system_context($newparent);
- require_capability('moodle/category:manage', $parentcontext);
- self::validate_context($parentcontext);
- category_delete_move($deletecat, $newparent, false);
+ self::validate_context(context_coursecat::instance($newparentcat->id));
+ if ($deletecat->can_move_content_to($newparentcat->id)) {
+ $deletecat->delete_move($newparentcat->id, false);
+ } else {
+ throw new moodle_exception('youcannotdeletecategory', '', '', $deletecat->get_formatted_name());
+ }
}
}
diff --git a/course/lib.php b/course/lib.php
index 347d17cde8f..be231715d15 100644
--- a/course/lib.php
+++ b/course/lib.php
@@ -1219,129 +1219,6 @@ function get_category_or_system_context($categoryid) {
}
}
-/**
- * Gets the child categories of a given courses category. Uses a static cache
- * to make repeat calls efficient.
- *
- * @param int $parentid the id of a course category.
- * @return array all the child course categories.
- */
-function get_child_categories($parentid) {
- static $allcategories = null;
-
- // only fill in this variable the first time
- if (null == $allcategories) {
- $allcategories = array();
-
- $categories = get_categories();
- foreach ($categories as $category) {
- if (empty($allcategories[$category->parent])) {
- $allcategories[$category->parent] = array();
- }
- $allcategories[$category->parent][] = $category;
- }
- }
-
- if (empty($allcategories[$parentid])) {
- return array();
- } else {
- return $allcategories[$parentid];
- }
-}
-
-/**
- * This function recursively travels the categories, building up a nice list
- * for display. It also makes an array that list all the parents for each
- * category.
- *
- * 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
- * $list = array(1 => 'Miscellaneous', 2 => 'Miscellaneous / Subcategory',
- * 4 => 'Miscellaneous / Subcategory / Sub-subcategory',
- * 3 => 'Other category');
- * $parents = array(2 => array(1), 4 => array(1, 2));
- *
- * If you specify $requiredcapability, then only categories where the current
- * user has that capability will be added to $list, although all categories
- * will still be added to $parents, and 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 $excluded, 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.
- *
- * @param array $list For output, accumulates an array categoryid => full category path name
- * @param array $parents For output, accumulates an array categoryid => list of parent category ids.
- * @param string/array $requiredcapability if given, only categories where the current
- * user has this capability will be added to $list. Can also be an array of capabilities,
- * in which case they are all required.
- * @param integer $excludeid Omit this category and its children from the lists built.
- * @param object $category Build the tree starting at this category - otherwise starts at the top level.
- * @param string $path For internal use, as part of recursive calls.
- */
-function make_categories_list(&$list, &$parents, $requiredcapability = '',
- $excludeid = 0, $category = NULL, $path = "") {
-
- // initialize the arrays if needed
- if (!is_array($list)) {
- $list = array();
- }
- if (!is_array($parents)) {
- $parents = array();
- }
-
- if (empty($category)) {
- // Start at the top level.
- $category = new stdClass;
- $category->id = 0;
- } else {
- // This is the excluded category, don't include it.
- if ($excludeid > 0 && $excludeid == $category->id) {
- return;
- }
-
- $context = context_coursecat::instance($category->id);
- $categoryname = format_string($category->name, true, array('context' => $context));
-
- // Update $path.
- if ($path) {
- $path = $path.' / '.$categoryname;
- } else {
- $path = $categoryname;
- }
-
- // Add this category to $list, if the permissions check out.
- if (empty($requiredcapability)) {
- $list[$category->id] = $path;
-
- } else {
- $requiredcapability = (array)$requiredcapability;
- if (has_all_capabilities($requiredcapability, $context)) {
- $list[$category->id] = $path;
- }
- }
- }
-
- // Add all the children recursively, while updating the parents array.
- if ($categories = get_child_categories($category->id)) {
- foreach ($categories as $cat) {
- if (!empty($category->id)) {
- if (isset($parents[$category->id])) {
- $parents[$cat->id] = $parents[$category->id];
- }
- $parents[$cat->id][] = $category->id;
- }
- make_categories_list($list, $parents, $requiredcapability, $excludeid, $cat, $path);
- }
- }
-}
-
/**
* This function generates a structured array of courses and categories.
*
@@ -1358,14 +1235,14 @@ function make_categories_list(&$list, &$parents, $requiredcapability = '',
*/
function get_course_category_tree($id = 0, $depth = 0) {
global $DB, $CFG;
- $viewhiddencats = has_capability('moodle/category:viewhiddencategories', context_system::instance());
- $categories = get_child_categories($id);
+ require_once($CFG->libdir. '/coursecatlib.php');
+ if (!$coursecat = coursecat::get($id, IGNORE_MISSING)) {
+ return array();
+ }
+ $categories = array();
$categoryids = array();
- foreach ($categories as $key => &$category) {
- if (!$category->visible && !$viewhiddencats) {
- unset($categories[$key]);
- continue;
- }
+ foreach ($coursecat->get_children() as $child) {
+ $categories[] = $category = (object)convert_to_array($child);
$categoryids[$category->id] = $category;
if (empty($CFG->maxcategorydepth) || $depth <= $CFG->maxcategorydepth) {
list($category->categories, $subcategories) = get_course_category_tree($category->id, $depth+1);
@@ -1417,37 +1294,31 @@ function get_course_category_tree($id = 0, $depth = 0) {
*/
function print_whole_category_list($category=NULL, $displaylist=NULL, $parentslist=NULL, $depth=-1, $showcourses = true, $categorycourses=NULL) {
global $CFG;
+ require_once($CFG->libdir. '/coursecatlib.php');
// maxcategorydepth == 0 meant no limit
if (!empty($CFG->maxcategorydepth) && $depth >= $CFG->maxcategorydepth) {
return;
}
- if (!$displaylist) {
- make_categories_list($displaylist, $parentslist);
+ // make sure category is visible to the current user
+ if ($category) {
+ if (!$coursecat = coursecat::get($category->id, IGNORE_MISSING)) {
+ return;
+ }
+ } else {
+ $coursecat = coursecat::get(0);
}
if (!$categorycourses) {
- if ($category) {
- $categorycourses = get_category_courses_array($category->id);
- } else {
- $categorycourses = get_category_courses_array();
- }
+ $categorycourses = get_category_courses_array($coursecat->id);
}
- if ($category) {
- if ($category->visible or has_capability('moodle/category:viewhiddencategories', context_system::instance())) {
- print_category_info($category, $depth, $showcourses, $categorycourses[$category->id]);
- } else {
- return; // Don't bother printing children of invisible categories
- }
-
- } else {
- $category = new stdClass();
- $category->id = "0";
+ if ($coursecat->id) {
+ print_category_info($category, $depth, $showcourses, $categorycourses[$category->id]);
}
- if ($categories = get_child_categories($category->id)) { // Print all the children recursively
+ if ($categories = $coursecat->get_children()) { // Print all the children recursively
$countcats = count($categories);
$count = 0;
$first = true;
@@ -1497,18 +1368,19 @@ function get_category_courses_array_recursively(array &$flattened, $category) {
}
/**
- * This function will return $options array for html_writer::select(), with whitespace to denote nesting.
+ * Returns full course categories trees to be used in html_writer::select()
+ *
+ * Calls {@link coursecat::make_categories_list()} to build the tree and
+ * adds whitespace to denote nesting
+ *
+ * @return array array mapping coursecat id to the display name
*/
function make_categories_options() {
- make_categories_list($cats,$parents);
+ global $CFG;
+ require_once($CFG->libdir. '/coursecatlib.php');
+ $cats = coursecat::make_categories_list();
foreach ($cats as $key => $value) {
- if (array_key_exists($key,$parents)) {
- if ($indent = count($parents[$key])) {
- for ($i = 0; $i < $indent; $i++) {
- $cats[$key] = ' '.$cats[$key];
- }
- }
- }
+ $cats[$key] = str_repeat(' ', coursecat::get($key)->depth - 1). $value;
}
return $cats;
}
@@ -1672,9 +1544,10 @@ function can_edit_in_category($categoryid = 0) {
*/
function print_courses($category) {
global $CFG, $OUTPUT;
+ require_once($CFG->libdir. '/coursecatlib.php');
if (!is_object($category) && $category==0) {
- $categories = get_child_categories(0); // Parent = 0 ie top-level categories only
+ $categories = coursecat::get(0)->get_children(); // Parent = 0 ie top-level categories only
if (is_array($categories) && count($categories) == 1) {
$category = array_shift($categories);
$courses = get_courses_wmanagers($category->id,
@@ -2732,112 +2605,6 @@ function course_allowed_module($course, $modname) {
return has_capability($capability, $coursecontext);
}
-/**
- * Recursively delete category including all subcategories and courses.
- * @param stdClass $category
- * @param boolean $showfeedback display some notices
- * @return array return deleted courses
- */
-function category_delete_full($category, $showfeedback=true) {
- global $CFG, $DB;
- require_once($CFG->libdir.'/gradelib.php');
- require_once($CFG->libdir.'/questionlib.php');
- require_once($CFG->dirroot.'/cohort/lib.php');
-
- if ($children = $DB->get_records('course_categories', array('parent'=>$category->id), 'sortorder ASC')) {
- foreach ($children as $childcat) {
- category_delete_full($childcat, $showfeedback);
- }
- }
-
- $deletedcourses = array();
- if ($courses = $DB->get_records('course', array('category'=>$category->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($category);
-
- // now delete anything that may depend on course category context
- grade_course_category_delete($category->id, 0, $showfeedback);
- if (!question_delete_course_category($category, 0, $showfeedback)) {
- throw new moodle_exception('cannotdeletecategoryquestions','','',$category->name);
- }
-
- // finally delete the category and it's context
- $DB->delete_records('course_categories', array('id'=>$category->id));
- delete_context(CONTEXT_COURSECAT, $category->id);
- add_to_log(SITEID, "category", "delete", "index.php", "$category->name (ID $category->id)");
-
- events_trigger('course_category_deleted', $category);
-
- return $deletedcourses;
-}
-
-/**
- * Delete category, but move contents to another category.
- * @param object $ccategory
- * @param int $newparentid category id
- * @return bool status
- */
-function category_delete_move($category, $newparentid, $showfeedback=true) {
- global $CFG, $DB, $OUTPUT;
- require_once($CFG->libdir.'/gradelib.php');
- require_once($CFG->libdir.'/questionlib.php');
- require_once($CFG->dirroot.'/cohort/lib.php');
-
- if (!$newparentcat = $DB->get_record('course_categories', array('id'=>$newparentid))) {
- return false;
- }
-
- if ($children = $DB->get_records('course_categories', array('parent'=>$category->id), 'sortorder ASC')) {
- foreach ($children as $childcat) {
- move_category($childcat, $newparentcat);
- }
- }
-
- if ($courses = $DB->get_records('course', array('category'=>$category->id), 'sortorder ASC', 'id')) {
- if (!move_courses(array_keys($courses), $newparentid)) {
- if ($showfeedback) {
- echo $OUTPUT->notification("Error moving courses");
- }
- return false;
- }
- if ($showfeedback) {
- echo $OUTPUT->notification(get_string('coursesmovedout', '', format_string($category->name)), 'notifysuccess');
- }
- }
-
- // move or delete cohorts in this context
- cohort_delete_category($category);
-
- // now delete anything that may depend on course category context
- grade_course_category_delete($category->id, $newparentid, $showfeedback);
- if (!question_delete_course_category($category, $newparentcat, $showfeedback)) {
- if ($showfeedback) {
- echo $OUTPUT->notification(get_string('errordeletingquestionsfromcategory', 'question', $category), 'notifysuccess');
- }
- return false;
- }
-
- // finally delete the category and it's context
- $DB->delete_records('course_categories', array('id'=>$category->id));
- delete_context(CONTEXT_COURSECAT, $category->id);
- add_to_log(SITEID, "category", "delete", "index.php", "$category->name (ID $category->id)");
-
- events_trigger('course_category_deleted', $category);
-
- if ($showfeedback) {
- echo $OUTPUT->notification(get_string('coursecategorydeleted', '', format_string($category->name)), 'notifysuccess');
- }
- return true;
-}
-
/**
* Efficiently moves many courses around while maintaining
* sortorder in order.
@@ -2882,98 +2649,11 @@ function move_courses($courseids, $categoryid) {
}
}
fix_course_sortorder();
+ cache_helper::purge_by_event('changesincourse');
return true;
}
-/**
- * Hide course category and child course and subcategories
- * @param stdClass $category
- * @return void
- */
-function course_category_hide($category) {
- global $DB;
-
- $category->visible = 0;
- $DB->set_field('course_categories', 'visible', 0, array('id'=>$category->id));
- $DB->set_field('course_categories', 'visibleold', 0, array('id'=>$category->id));
- $DB->execute("UPDATE {course} SET visibleold = visible WHERE category = ?", array($category->id)); // store visible flag so that we can return to it if we immediately unhide
- $DB->set_field('course', 'visible', 0, array('category' => $category->id));
- // get all child categories and hide too
- if ($subcats = $DB->get_records_select('course_categories', "path LIKE ?", array("$category->path/%"))) {
- 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));
- }
- }
- add_to_log(SITEID, "category", "hide", "editcategory.php?id=$category->id", $category->id);
-}
-
-/**
- * Show course category and child course and subcategories
- * @param stdClass $category
- * @return void
- */
-function course_category_show($category) {
- global $DB;
-
- $category->visible = 1;
- $DB->set_field('course_categories', 'visible', 1, array('id'=>$category->id));
- $DB->set_field('course_categories', 'visibleold', 1, array('id'=>$category->id));
- $DB->execute("UPDATE {course} SET visible = visibleold WHERE category = ?", array($category->id));
- // get all child categories and unhide too
- if ($subcats = $DB->get_records_select('course_categories', "path LIKE ?", array("$category->path/%"))) {
- 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));
- }
- }
- add_to_log(SITEID, "category", "show", "editcategory.php?id=$category->id", $category->id);
-}
-
-/**
- * Efficiently moves a category - NOTE that this can have
- * a huge impact access-control-wise...
- */
-function move_category($category, $newparentcat) {
- global $CFG, $DB;
-
- $context = context_coursecat::instance($category->id);
-
- $hidecat = false;
- if (empty($newparentcat->id)) {
- $DB->set_field('course_categories', 'parent', 0, array('id' => $category->id));
- $newparent = context_system::instance();
- } else {
- $DB->set_field('course_categories', 'parent', $newparentcat->id, array('id' => $category->id));
- $newparent = context_coursecat::instance($newparentcat->id);
-
- if (!$newparentcat->visible and $category->visible) {
- // better hide category when moving into hidden category, teachers may unhide afterwards and the hidden children will be restored properly
- $hidecat = true;
- }
- }
-
- context_moved($context, $newparent);
-
- // now make it last in new category
- $DB->set_field('course_categories', 'sortorder', MAX_COURSES_IN_CATEGORY*MAX_COURSE_CATEGORIES, array('id'=>$category->id));
-
- // Log action.
- add_to_log(SITEID, "category", "move", "editcategory.php?id=$category->id", $category->id);
-
- // and fix the sortorders
- fix_course_sortorder();
-
- if ($hidecat) {
- course_category_hide($category);
- }
-}
-
/**
* Returns the display name of the given section that the course prefers
*
@@ -3149,6 +2829,8 @@ function create_course($data, $editoroptions = NULL) {
course_create_sections_if_missing($course, 0);
fix_course_sortorder();
+ // purge appropriate caches in case fix_course_sortorder() did not change anything
+ cache_helper::purge_by_event('changesincourse');
// new context created - better mark it as dirty
mark_context_dirty($context->path);
@@ -3167,32 +2849,6 @@ function create_course($data, $editoroptions = NULL) {
return $course;
}
-/**
- * Create a new course category and marks the context as dirty
- *
- * This function does not set the sortorder for the new category and
- * @see{fix_course_sortorder} should be called after creating a new course
- * category
- *
- * Please note that this function does not verify access control.
- *
- * @param object $category All of the data required for an entry in the course_categories table
- * @return object new course category
- */
-function create_course_category($category) {
- global $DB;
-
- $category->timemodified = time();
- $category->id = $DB->insert_record('course_categories', $category);
- $category = $DB->get_record('course_categories', array('id' => $category->id));
-
- // We should mark the context as dirty
- $category->context = context_coursecat::instance($category->id);
- $category->context->mark_dirty();
-
- return $category;
-}
-
/**
* Update a course.
*
@@ -3255,6 +2911,8 @@ function update_course($data, $editoroptions = NULL) {
}
fix_course_sortorder();
+ // purge appropriate caches in case fix_course_sortorder() did not change anything
+ cache_helper::purge_by_event('changesincourse');
// Test for and remove blocks which aren't appropriate anymore
blocks_remove_inappropriate($course);
@@ -3555,6 +3213,31 @@ class course_request {
return $this->properties->collision;
}
+ /**
+ * Returns the category where this course request should be created
+ *
+ * Note that we don't check here that user has a capability to view
+ * hidden categories if he has capabilities 'moodle/site:approvecourse' and
+ * 'moodle/course:changecategory'
+ *
+ * @return coursecat
+ */
+ public function get_category() {
+ global $CFG;
+ require_once($CFG->libdir.'/coursecatlib.php');
+ // If the category is not set, if the current user does not have the rights to change the category, or if the
+ // category does not exist, we set the default category to the course to be approved.
+ // The system level is used because the capability moodle/site:approvecourse is based on a system level.
+ if (empty($this->properties->category) || !has_capability('moodle/course:changecategory', context_system::instance()) ||
+ (!$category = coursecat::get($this->properties->category, IGNORE_MISSING, true))) {
+ $category = coursecat::get($CFG->defaultrequestcategory, IGNORE_MISSING, true);
+ }
+ if (!$category) {
+ $category = coursecat::get_default();
+ }
+ return $category;
+ }
+
/**
* This function approves the request turning it into a course
*
@@ -3577,18 +3260,9 @@ class course_request {
unset($data->reason);
unset($data->requester);
- // If the category is not set, if the current user does not have the rights to change the category, or if the
- // category does not exist, we set the default category to the course to be approved.
- // The system level is used because the capability moodle/site:approvecourse is based on a system level.
- if (empty($data->category) || !has_capability('moodle/course:changecategory', context_system::instance()) ||
- (!$category = get_course_category($data->category))) {
- $category = get_course_category($CFG->defaultrequestcategory);
- }
-
// Set category
+ $category = $this->get_category();
$data->category = $category->id;
- $data->sortorder = $category->sortorder; // place as the first in category
-
// Set misc settings
$data->requested = 1;
diff --git a/course/manage.php b/course/manage.php
index 909ce78fe0e..128e7a7a18a 100644
--- a/course/manage.php
+++ b/course/manage.php
@@ -24,6 +24,7 @@
require_once("../config.php");
require_once($CFG->dirroot.'/course/lib.php');
+require_once($CFG->libdir.'/coursecatlib.php');
// Category id.
$id = optional_param('id', 0, PARAM_INT);
@@ -51,18 +52,17 @@ $movedowncat = optional_param('movedowncat', 0, PARAM_INT);
require_login();
+// Retrieve coursecat object
+// This will also make sure that category is accessible and create default category if missing
+$coursecat = coursecat::get($id);
+
if ($id) {
$PAGE->set_category_by_id($id);
$PAGE->set_url(new moodle_url('/course/manage.php', array('id' => $id)));
// This is sure to be the category context.
$context = $PAGE->context;
- // And the object has been loaded for us no need for another DB call.
- $category = $PAGE->category;
- if (!can_edit_in_category($category->id)) {
- redirect(new moodle_url('/course/category.php', array('id' => $category->id)));
- }
- if (!$category->visible) {
- require_capability('moodle/category:viewhiddencategories', $context);
+ if (!can_edit_in_category($coursecat->id)) {
+ redirect(new moodle_url('/course/category.php', array('id' => $coursecat->id)));
}
} else {
$context = context_system::instance();
@@ -75,26 +75,10 @@ if ($id) {
$canmanage = has_capability('moodle/category:manage', $context);
-// Check the default category exists.
-if (!$id && !$DB->record_exists('course_categories', array('parent' => 0))) {
- // No category yet! Try and make one.
- $tempcat = new stdClass;
- $tempcat->name = get_string('miscellaneous');
- $tempcat->id = $DB->insert_record('course_categories', $tempcat);
- // Fetch the context to ensure it is created.
- context_coursecat::instance($tempcat->id);
- mark_context_dirty('/'.SYSCONTEXTID);
- // Required to build course_categories.depth and categories.path.
- fix_course_sortorder();
- set_config('defaultrequestcategory', $tempcat->id);
- // Unset the temp category. We no longer need it.
- unset($tempcat);
-}
-
// Process any category actions.
if (!empty($deletecat) and confirm_sesskey()) {
// Delete a category.
- $cattodelete = $DB->get_record('course_categories', array('id' => $deletecat), '*', MUST_EXIST);
+ $cattodelete = coursecat::get($deletecat);
$context = context_coursecat::instance($deletecat);
require_capability('moodle/category:manage', $context);
require_capability('moodle/category:manage', get_category_or_system_context($cattodelete->parent));
@@ -103,7 +87,6 @@ if (!empty($deletecat) and confirm_sesskey()) {
require_once($CFG->dirroot.'/course/delete_category_form.php');
$mform = new delete_category_form(null, $cattodelete);
- $mform->set_data(array('deletecat' => $deletecat));
if ($mform->is_cancelled()) {
redirect(new moodle_url('/course/manage.php'));
}
@@ -114,25 +97,24 @@ if (!empty($deletecat) and confirm_sesskey()) {
if ($data = $mform->get_data()) {
// The form has been submit handle it.
- if ($data->fulldelete) {
- $deletedcourses = category_delete_full($cattodelete, true);
+ if ($data->fulldelete == 1 && $cattodelete->can_delete_full()) {
+ $cattodeletename = $cattodelete->get_formatted_name();
+ $deletedcourses = $cattodelete->delete_full(true);
foreach ($deletedcourses as $course) {
echo $OUTPUT->notification(get_string('coursedeleted', '', $course->shortname), 'notifysuccess');
}
- $cattodeletename = format_string($cattodelete->name, true, array('context' => $context));
echo $OUTPUT->notification(get_string('coursecategorydeleted', '', $cattodeletename), 'notifysuccess');
+ echo $OUTPUT->continue_button(new moodle_url('/course/manage.php'));
+ } else if ($data->fulldelete == 0 && $cattodelete->can_move_content_to($data->newparent)) {
+ $cattodelete->delete_move($data->newparent, true);
+ echo $OUTPUT->continue_button(new moodle_url('/course/manage.php'));
} else {
- category_delete_move($cattodelete, $data->newparent, true);
+ // Some error in parameters (user is cheating?)
+ $mform->display();
}
- if ($deletecat == $CFG->defaultrequestcategory) {
- // If we deleted $CFG->defaultrequestcategory, make it point somewhere else.
- set_config('defaultrequestcategory', $DB->get_field('course_categories', 'MIN(id)', array('parent' => 0)));
- }
- echo $OUTPUT->continue_button(new moodle_url('/course/manage.php'));
} else {
// Display the form.
- require_once($CFG->libdir . '/questionlib.php');
$mform->display();
}
// Finish output and exit.
@@ -142,31 +124,25 @@ if (!empty($deletecat) and confirm_sesskey()) {
if (!empty($movecat) and ($movetocat >= 0) and confirm_sesskey()) {
// Move a category to a new parent if required.
- if ($cattomove = $DB->get_record('course_categories', array('id' => $movecat))) {
- require_capability('moodle/category:manage', get_category_or_system_context($cattomove->parent));
- if ($cattomove->parent != $movetocat) {
- $newparent = $DB->get_record('course_categories', array('id' => $movetocat));
- require_capability('moodle/category:manage', get_category_or_system_context($movetocat));
- move_category($cattomove, $newparent);
+ $cattomove = coursecat::get($movecat);
+ if ($cattomove->parent != $movetocat) {
+ if ($cattomove->can_change_parent($movetocat)) {
+ $cattomove->change_parent($movetocat);
+ } else {
+ print_error('cannotmovecategory');
}
}
}
// Hide or show a category.
if ($hidecat and confirm_sesskey()) {
- if ($tempcat = $DB->get_record('course_categories', array('id' => $hidecat))) {
- require_capability('moodle/category:manage', get_category_or_system_context($tempcat->parent));
- if ($tempcat->visible == 1) {
- course_category_hide($tempcat);
- }
- }
+ $cattohide = coursecat::get($hidecat);
+ require_capability('moodle/category:manage', get_category_or_system_context($cattohide->parent));
+ $cattohide->hide();
} else if ($showcat and confirm_sesskey()) {
- if ($tempcat = $DB->get_record('course_categories', array('id' => $showcat))) {
- require_capability('moodle/category:manage', get_category_or_system_context($tempcat->parent));
- if ($tempcat->visible == 0) {
- course_category_show($tempcat);
- }
- }
+ $cattoshow = coursecat::get($showcat);
+ require_capability('moodle/category:manage', get_category_or_system_context($cattoshow->parent));
+ $cattoshow->show();
}
if ((!empty($moveupcat) or !empty($movedowncat)) and confirm_sesskey()) {
@@ -201,17 +177,18 @@ if ((!empty($moveupcat) or !empty($movedowncat)) and confirm_sesskey()) {
fix_course_sortorder();
}
-if (isset($category) && $canmanage && $resort && confirm_sesskey()) {
+if ($coursecat->id && $canmanage && $resort && confirm_sesskey()) {
// Resort the category.
- if ($courses = get_courses($category->id, '', 'c.id,c.fullname,c.sortorder')) {
+ if ($courses = get_courses($coursecat->id, '', 'c.id,c.fullname,c.sortorder')) {
collatorlib::asort_objects_by_property($courses, 'fullname', collatorlib::SORT_NATURAL);
$i = 1;
foreach ($courses as $course) {
- $DB->set_field('course', 'sortorder', $category->sortorder + $i, array('id' => $course->id));
+ $DB->set_field('course', 'sortorder', $coursecat->sortorder + $i, array('id' => $course->id));
$i++;
}
// This should not be needed but we do it just to be safe.
fix_course_sortorder();
+ cache_helper::purge_by_event('changesincourse');
}
}
@@ -257,6 +234,7 @@ if ((!empty($hide) or !empty($show)) && confirm_sesskey()) {
// Set the visibility of the course. we set the old flag when user manually changes visibility of course.
$params = array('id' => $course->id, 'visible' => $visible, 'visibleold' => $visible, 'timemodified' => time());
$DB->update_record('course', $params);
+ cache_helper::purge_by_event('changesincourse');
add_to_log($course->id, "course", ($visible ? 'show' : 'hide'), "edit.php?id=$course->id", $course->id);
}
@@ -284,6 +262,7 @@ if ((!empty($moveup) or !empty($movedown)) && confirm_sesskey()) {
}
$DB->set_field('course', 'sortorder', $swapcourse->sortorder, array('id' => $movecourse->id));
$DB->set_field('course', 'sortorder', $movecourse->sortorder, array('id' => $swapcourse->id));
+ cache_helper::purge_by_event('changesincourse');
add_to_log($movecourse->id, "course", "move", "edit.php?id=$movecourse->id", $movecourse->id);
}
}
@@ -314,24 +293,18 @@ if (can_edit_in_category()) {
$PAGE->navbar->add($settingsnode->text, $settingsnode->action);
}
} else {
- // If we get here then they must have arrived here using a specific category
- // within which they can manage.
- // We can safetly assume $category is set.
$site = get_site();
- $PAGE->set_title("$site->shortname: $category->name");
+ $PAGE->set_title("$site->shortname: $coursecat->name");
$PAGE->set_heading($site->fullname);
$PAGE->set_button(print_course_search('', true, 'navbar'));
}
-$parentlist = array();
-$displaylist = array();
-make_categories_list($displaylist, $parentlist);
$displaylist[0] = get_string('top');
// Start output.
echo $OUTPUT->header();
-if (!isset($category)) {
+if (!$coursecat->id) {
// Print out the categories with all the knobs.
$table = new html_table;
$table->id = 'coursecategories';
@@ -350,12 +323,12 @@ if (!isset($category)) {
);
$table->data = array();
- print_category_edit($table, null, $displaylist, $parentlist);
+ print_category_edit($table, $coursecat);
echo html_writer::table($table);
} else {
// Print the category selector.
- $select = new single_select(new moodle_url('/course/manage.php'), 'id', $displaylist, $category->id, null, 'switchcategory');
+ $select = new single_select(new moodle_url('/course/manage.php'), 'id', $displaylist, $coursecat->id, null, 'switchcategory');
$select->set_label(get_string('categories').':');
echo html_writer::start_tag('div', array('class' => 'categorypicker'));
@@ -382,7 +355,7 @@ if ($canmanage) {
echo $OUTPUT->container_end();
}
-if (isset($category)) {
+if ($coursecat->id) {
// Print out all the sub-categories (plain mode).
// In order to view hidden subcategories the user must have the viewhiddencategories.
// capability in the current category..
@@ -401,7 +374,7 @@ if (isset($category)) {
ctx.contextlevel = :contextlevel
$categorywhere
ORDER BY cc.sortorder ASC";
- $subcategories = $DB->get_recordset_sql($sql, array('parentid' => $category->id, 'contextlevel' => CONTEXT_COURSECAT));
+ $subcategories = $DB->get_recordset_sql($sql, array('parentid' => $coursecat->id, 'contextlevel' => CONTEXT_COURSECAT));
// Prepare a table to display the sub categories.
$table = new html_table;
$table->attributes = array(
@@ -430,7 +403,7 @@ if (isset($category)) {
echo html_writer::table($table);
}
- $courses = get_courses_page($category->id, 'c.sortorder ASC',
+ $courses = get_courses_page($coursecat->id, 'c.sortorder ASC',
'c.id,c.sortorder,c.shortname,c.fullname,c.summary,c.visible',
$totalcount, $page*$perpage, $perpage);
$numcourses = count($courses);
@@ -552,9 +525,7 @@ if (!$courses) {
}
if ($abletomovecourses) {
- $movetocategories = array();
- $notused = array();
- make_categories_list($movetocategories, $notused, 'moodle/category:manage');
+ $movetocategories = coursecat::make_categories_list('moodle/category:manage');
$movetocategories[$id] = get_string('moveselectedcoursesto');
$cell = new html_table_cell();
@@ -593,8 +564,8 @@ if ($canmanage and $numcourses > 1) {
if (has_capability('moodle/course:create', $context)) {
// Print button to create a new course.
$url = new moodle_url('/course/edit.php');
- if (isset($category)) {
- $url->params(array('category' => $category->id, 'returnto' => 'catmanage'));
+ if ($coursecat->id) {
+ $url->params(array('category' => $coursecat->id, 'returnto' => 'catmanage'));
} else {
$url->params(array('category' => $CFG->defaultrequestcategory, 'returnto' => 'topcatmanage'));
}
@@ -614,14 +585,12 @@ echo $OUTPUT->footer();
* Recursive function to print all the categories ready for editing.
*
* @param html_table $table The table to add data to.
- * @param stdClass $category The category to render
- * @param array $displaylist The categories this can be moved to.
- * @param array $parentslist An array of categories.
+ * @param coursecat $category The category to render
* @param int $depth The depth of the category.
* @param bool $up True if this category can be moved up.
* @param bool $down True if this category can be moved down.
*/
-function print_category_edit(html_table $table, $category, $displaylist, $parentslist, $depth=-1, $up=false, $down=false) {
+function print_category_edit(html_table $table, coursecat $category, $depth = -1, $up = false, $down = false) {
global $OUTPUT;
static $str = null;
@@ -639,22 +608,20 @@ function print_category_edit(html_table $table, $category, $displaylist, $parent
$str->spacer = $OUTPUT->spacer().' ';
}
- if (!empty($category)) {
+ if ($category->id) {
- if (!isset($category->context)) {
- $category->context = context_coursecat::instance($category->id);
- }
+ $categorycontext = context_coursecat::instance($category->id);
$attributes = array();
$attributes['class'] = $category->visible ? '' : 'dimmed';
$attributes['title'] = $str->edit;
$categoryurl = new moodle_url('/course/manage.php', array('id' => $category->id, 'sesskey' => sesskey()));
- $categoryname = format_string($category->name, true, array('context' => $category->context));
+ $categoryname = $category->get_formatted_name();
$categorypadding = str_repeat(' ', $depth);
$categoryname = $categorypadding . html_writer::link($categoryurl, $categoryname, $attributes);
$icons = array();
- if (has_capability('moodle/category:manage', $category->context)) {
+ if (has_capability('moodle/category:manage', $categorycontext)) {
// Edit category.
$icons[] = $OUTPUT->action_icon(
new moodle_url('/course/editcategory.php', array('id' => $category->id)),
@@ -682,9 +649,9 @@ function print_category_edit(html_table $table, $category, $displaylist, $parent
);
}
// Cohorts.
- if (has_any_capability(array('moodle/cohort:manage', 'moodle/cohort:view'), $category->context)) {
+ if (has_any_capability(array('moodle/cohort:manage', 'moodle/cohort:view'), $categorycontext)) {
$icons[] = $OUTPUT->action_icon(
- new moodle_url('/cohort/index.php', array('contextid' => $category->context->id)),
+ new moodle_url('/cohort/index.php', array('contextid' => $categorycontext->id)),
new pix_icon('t/cohort', $str->cohorts, 'moodle', array('class' => 'iconsmall')),
null, array('title' => $str->cohorts)
);
@@ -711,15 +678,9 @@ function print_category_edit(html_table $table, $category, $displaylist, $parent
}
$actions = '';
- if (has_capability('moodle/category:manage', $category->context)) {
- $tempdisplaylist = $displaylist;
- unset($tempdisplaylist[$category->id]);
- foreach ($parentslist as $key => $parents) {
- if (in_array($category->id, $parents)) {
- unset($tempdisplaylist[$key]);
- }
- }
+ if (has_capability('moodle/category:manage', $categorycontext)) {
$popupurl = new moodle_url("manage.php?movecat=$category->id&sesskey=".sesskey());
+ $tempdisplaylist = array(0 => get_string('top')) + coursecat::make_categories_list('moodle/category:manage', $category->id);
$select = new single_select($popupurl, 'movetocat', $tempdisplaylist, $category->parent, null, "moveform$category->id");
$select->set_label(get_string('frontpagecategorynames'), array('class' => 'accesshide'));
$actions = $OUTPUT->render($select);
@@ -735,14 +696,9 @@ function print_category_edit(html_table $table, $category, $displaylist, $parent
// Actions.
new html_table_cell($actions)
));
-
- // Get the subcategories to be printed.
- $categories = get_categories($category->id);
- } else {
- $categories = get_categories(0);
}
- if ($categories) {
+ if ($categories = $category->get_children()) {
// Print all the children recursively.
$countcats = count($categories);
$count = 0;
@@ -757,7 +713,7 @@ function print_category_edit(html_table $table, $category, $displaylist, $parent
$down = $last ? false : true;
$first = false;
- print_category_edit($table, $cat, $displaylist, $parentslist, $depth+1, $up, $down);
+ print_category_edit($table, $cat, $depth+1, $up, $down);
}
}
}
diff --git a/course/pending.php b/course/pending.php
index 3d6cb18c716..eaf4c285fc1 100644
--- a/course/pending.php
+++ b/course/pending.php
@@ -110,23 +110,14 @@ if (empty($pending)) {
// Check here for shortname collisions and warn about them.
$course->check_shortname_collision();
- // Retreiving category name.
- // If the category was not set (can happen after upgrade) or if the user does not have the capability
- // to change the category, we fallback on the default one.
- // Else, the category proposed is fetched, but we fallback on the default one if we can't find it.
- // It is just a matter of displaying the right information because the logic when approving the category
- // proceeds the same way. The system context level is used as moodle/site:approvecourse uses it.
- if (empty($course->category) || !has_capability('moodle/course:changecategory', context_system::instance()) ||
- (!$category = get_course_category($course->category))) {
- $category = get_course_category($CFG->defaultrequestcategory);
- }
+ $category = $course->get_category();
$row = array();
$row[] = format_string($course->shortname);
$row[] = format_string($course->fullname);
$row[] = fullname($course->get_requester());
$row[] = $course->summary;
- $row[] = format_string($category->name);
+ $row[] = $category->get_formatted_name();
$row[] = format_string($course->reason);
$row[] = $OUTPUT->single_button(new moodle_url($baseurl, array('approve' => $course->id, 'sesskey' => sesskey())), get_string('approve'), 'get') .
$OUTPUT->single_button(new moodle_url($baseurl, array('reject' => $course->id)), get_string('rejectdots'), 'get');
diff --git a/course/request_form.php b/course/request_form.php
index 867a684e97f..620284ca1c2 100644
--- a/course/request_form.php
+++ b/course/request_form.php
@@ -36,6 +36,7 @@ if (!defined('MOODLE_INTERNAL')) {
}
require_once($CFG->libdir.'/formslib.php');
+require_once($CFG->libdir.'/coursecatlib.php');
/**
* A form for a user to request a course.
@@ -69,9 +70,7 @@ class course_request_form extends moodleform {
$mform->setType('shortname', PARAM_TEXT);
if (!empty($CFG->requestcategoryselection)) {
- $displaylist = array();
- $parentlist = array();
- make_categories_list($displaylist, $parentlist, '');
+ $displaylist = coursecat::make_categories_list();
$mform->addElement('select', 'category', get_string('category'), $displaylist);
$mform->setDefault('category', $CFG->defaultrequestcategory);
$mform->addHelpButton('category', 'category');
diff --git a/course/search.php b/course/search.php
index aeccc4f5cd2..38bac692e86 100644
--- a/course/search.php
+++ b/course/search.php
@@ -24,6 +24,7 @@
require_once("../config.php");
require_once($CFG->dirroot.'/course/lib.php');
+require_once($CFG->libdir.'/coursecatlib.php');
$search = optional_param('search', '', PARAM_RAW); // search words
$page = optional_param('page', 0, PARAM_INT); // which page to show
@@ -38,14 +39,8 @@ $modulelist= optional_param('modulelist', '', PARAM_PLUGIN);
// List of minimum capabilities which user need to have for editing/moving course
$capabilities = array('moodle/course:create', 'moodle/category:manage');
-// List of category id's in which current user has course:create and category:manage capability.
-$usercatlist = array();
-
-// List of parent category id's
-$catparentlist = array();
-
-// Populate usercatlist with list of category id's with required capabilities.
-make_categories_list($usercatlist, $catparentlist, $capabilities);
+// Populate usercatlist with list of category id's with course:create and category:manage capabilities.
+$usercatlist = coursecat::make_categories_list($capabilities);
$search = trim(strip_tags($search)); // trim & clean raw searched string
if ($search) {
@@ -109,9 +104,7 @@ if (has_capability('moodle/course:visibility', context_system::instance())) {
}
}
-$displaylist = array();
-$parentlist = array();
-make_categories_list($displaylist, $parentlist);
+$displaylist = coursecat::make_categories_list();
$strcourses = new lang_string("courses");
$strsearch = new lang_string("search");
diff --git a/course/tests/courselib_test.php b/course/tests/courselib_test.php
index 6fc792a2453..f63d8b72dab 100644
--- a/course/tests/courselib_test.php
+++ b/course/tests/courselib_test.php
@@ -785,54 +785,6 @@ class courselib_testcase extends advanced_testcase {
$this->assertEquals('FROG101 Introduction to pond life', get_course_display_name_for_list($course));
}
- public function test_create_course_category() {
- global $CFG, $DB;
- $this->resetAfterTest(true);
-
- // Create the category
- $data = new stdClass();
- $data->name = 'aaa';
- $data->description = 'aaa';
- $data->idnumber = '';
-
- $category1 = create_course_category($data);
-
- // Initially confirm that base data was inserted correctly
- $this->assertEquals($data->name, $category1->name);
- $this->assertEquals($data->description, $category1->description);
- $this->assertEquals($data->idnumber, $category1->idnumber);
-
- // sortorder should be blank initially
- $this->assertEmpty($category1->sortorder);
-
- // Calling fix_course_sortorder() should provide a new sortorder
- fix_course_sortorder();
- $category1 = $DB->get_record('course_categories', array('id' => $category1->id));
-
- $this->assertGreaterThanOrEqual(1, $category1->sortorder);
-
- // Create two more categories and test the sortorder worked correctly
- $data->name = 'ccc';
- $category2 = create_course_category($data);
- $this->assertEmpty($category2->sortorder);
-
- $data->name = 'bbb';
- $category3 = create_course_category($data);
- $this->assertEmpty($category3->sortorder);
-
- // Calling fix_course_sortorder() should provide a new sortorder to give category1,
- // category2, category3. New course categories are ordered by id not name
- fix_course_sortorder();
-
- $category1 = $DB->get_record('course_categories', array('id' => $category1->id));
- $category2 = $DB->get_record('course_categories', array('id' => $category2->id));
- $category3 = $DB->get_record('course_categories', array('id' => $category3->id));
-
- $this->assertGreaterThanOrEqual($category1->sortorder, $category2->sortorder);
- $this->assertGreaterThanOrEqual($category2->sortorder, $category3->sortorder);
- $this->assertGreaterThanOrEqual($category1->sortorder, $category3->sortorder);
- }
-
public function test_move_module_in_course() {
global $DB;
diff --git a/enrol/database/settings.php b/enrol/database/settings.php
index 4682f2e8573..0a8d906c98d 100644
--- a/enrol/database/settings.php
+++ b/enrol/database/settings.php
@@ -108,12 +108,7 @@ if ($ADMIN->fulltree) {
$settings->add(new admin_setting_configtext('enrol_database/newcoursecategory', get_string('newcoursecategory', 'enrol_database'), '', ''));
if (!during_initial_install()) {
- require_once($CFG->dirroot.'/course/lib.php');
- $options = array();
- $parentlist = array();
- make_categories_list($options, $parentlist);
- $settings->add(new admin_setting_configselect('enrol_database/defaultcategory', get_string('defaultcategory', 'enrol_database'), get_string('defaultcategory_desc', 'enrol_database'), 1, $options));
- unset($parentlist);
+ $settings->add(new admin_setting_configselect('enrol_database/defaultcategory', get_string('defaultcategory', 'enrol_database'), get_string('defaultcategory_desc', 'enrol_database'), 1, make_categories_options()));
}
$settings->add(new admin_setting_configtext('enrol_database/templatecourse', get_string('templatecourse', 'enrol_database'), get_string('templatecourse_desc', 'enrol_database'), ''));
diff --git a/enrol/ldap/settings.php b/enrol/ldap/settings.php
index 7bb88940b84..c27ea279fdf 100644
--- a/enrol/ldap/settings.php
+++ b/enrol/ldap/settings.php
@@ -94,10 +94,7 @@ if ($ADMIN->fulltree) {
$options = $yesno;
$settings->add(new admin_setting_configselect('enrol_ldap/autocreate', get_string('autocreate_key', 'enrol_ldap'), get_string('autocreate', 'enrol_ldap'), 0, $options));
if (!during_initial_install()) {
- require_once($CFG->dirroot.'/course/lib.php');
- $parentlist = array();
- $options = array();
- make_categories_list($options, $parentlist);
+ $options = make_categories_options();
$settings->add(new admin_setting_configselect('enrol_ldap/category', get_string('category_key', 'enrol_ldap'), get_string('category', 'enrol_ldap'), key($options), $options));
}
$settings->add(new admin_setting_configtext_trim_lower('enrol_ldap/template', get_string('template_key', 'enrol_ldap'), get_string('template', 'enrol_ldap'), ''));
diff --git a/lang/en/cache.php b/lang/en/cache.php
index aec3fe82e2a..d081b61cb28 100644
--- a/lang/en/cache.php
+++ b/lang/en/cache.php
@@ -36,6 +36,9 @@ $string['cacheadmin'] = 'Cache administration';
$string['cacheconfig'] = 'Configuration';
$string['cachedef_calendar_subscriptions'] = 'Calendar subscriptions';
$string['cachedef_config'] = 'Config settings';
+$string['cachedef_coursecat'] = 'Course categories lists for particular user';
+$string['cachedef_coursecatrecords'] = 'Course categories records';
+$string['cachedef_coursecattree'] = 'Course categories tree';
$string['cachedef_databasemeta'] = 'Database meta information';
$string['cachedef_eventinvalidation'] = 'Event invalidation';
$string['cachedef_groupdata'] = 'Course group information';
diff --git a/lang/en/error.php b/lang/en/error.php
index 81544ccabee..622b2473463 100644
--- a/lang/en/error.php
+++ b/lang/en/error.php
@@ -105,6 +105,7 @@ $string['cannotmarktopic'] = 'Could not mark that topic for this course';
$string['cannotmigratedatacomments'] = 'Cannot migrate data module comments';
$string['cannotmodulename'] = 'Cannot get the module name in build navigation';
$string['cannotmoduletype'] = 'Cannot get the module type in build navigation';
+$string['cannotmovecategory'] = 'Cannot move category';
$string['cannotmoverolewithid'] = 'Cannot move role with ID {$a}';
$string['cannotopencsv'] = 'Cannot open CSV file';
$string['cannotopenfile'] = 'Cannot open file ({$a})';
@@ -159,7 +160,9 @@ $string['cannotviewprofile'] = 'You cannot view the profile of this user';
$string['cannotviewreport'] = 'You cannot view this report';
$string['cannotwritefile'] = 'Cannot write to file ({$a})';
$string['categoryerror'] = 'Category error';
+$string['categorynamerequired'] = 'Category name is required';
$string['categorytoolong'] = 'Category name too long';
+$string['categoryidnumbertaken'] = 'ID number is already used for another category';
$string['commentmisconf'] = 'Comment ID is misconfigured';
$string['componentisuptodate'] = 'Component is up-to-date';
$string['confirmsesskeybad'] = 'Sorry, but your session key could not be confirmed to carry out this action. This security feature prevents against accidental or malicious execution of important functions in your name. Please make sure you really wanted to execute this function.';
diff --git a/lib/coursecatlib.php b/lib/coursecatlib.php
new file mode 100644
index 00000000000..30752cea582
--- /dev/null
+++ b/lib/coursecatlib.php
@@ -0,0 +1,2121 @@
+.
+
+/**
+ * Contains class coursecat reponsible 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
+ *
+ * @package core
+ * @subpackage course
+ * @copyright 2013 Marina Glancy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class coursecat implements renderable, cacheable_object, IteratorAggregate {
+ /** @var coursecat stores pseudo category with id=0. Use coursecat::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' => null, // not cached
+ '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;
+
+ // ====== magic methods =======
+
+ /**
+ * 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 coursecat 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(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 coursecat 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 coursecat 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 coursecat::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 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!
+ * @return null|coursecat
+ */
+ public static function get($id, $strictness = MUST_EXIST, $alwaysreturnhidden = false) {
+ if (!$id) {
+ if (!isset(self::$coursecat0)) {
+ $record = new stdClass();
+ $record->id = 0;
+ $record->visible = 1;
+ $record->depth = 0;
+ $record->path = '';
+ self::$coursecat0 = new coursecat($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 coursecat($record);
+ // Store in cache
+ $coursecatrecordcache->set($id, $coursecat);
+ }
+ }
+ if ($coursecat && ($alwaysreturnhidden || $coursecat->is_uservisible())) {
+ return $coursecat;
+ } else {
+ if ($strictness == MUST_EXIST) {
+ throw new moodle_exception('unknowcategory');
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 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 coursecat
+ */
+ 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 coursecat
+ * @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 (textlib::strlen($data->name) > 255) {
+ throw new moodle_exception('categorytoolong');
+ }
+ $newcategory->name = $data->name;
+
+ // validate and set idnumber
+ if (!empty($data->idnumber)) {
+ if (textlib::strlen($data->idnumber) > 100) {
+ throw new moodle_exception('idnumbertoolong');
+ }
+ if ($DB->record_exists('course_categories', array('idnumber' => $data->idnumber))) {
+ throw new moodle_exception('categoryidnumbertaken');
+ }
+ }
+ if (isset($data->idnumber)) {
+ $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);
+ }
+
+ add_to_log(SITEID, "category", 'add', "editcategory.php?id=$newcategory->id", $newcategory->id);
+ 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 coursecat::change_parent_raw if field 'parent' is updated.
+ * It also calls coursecat::hide_raw or coursecat::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 coursecat::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 (textlib::strlen($data->name) > 255) {
+ throw new moodle_exception('categorytoolong');
+ }
+ $newcategory->name = $data->name;
+ }
+
+ if (isset($data->idnumber) && $data->idnumber != $this->idnumber) {
+ if (textlib::strlen($data->idnumber) > 100) {
+ throw new moodle_exception('idnumbertoolong');
+ }
+ if ($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();
+
+ if ($editoroptions) {
+ $categorycontext = context_coursecat::instance($this->id);
+ $newcategory = file_postupdate_standard_editor($newcategory, 'description', $editoroptions, $categorycontext, 'coursecat', 'description', 0);
+ }
+ $DB->update_record('course_categories', $newcategory);
+ add_to_log(SITEID, "category", 'update', "editcategory.php?id=$this->id", $this->id);
+ 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 current user
+ *
+ * Please note that methods coursecat::get (without 3rd argumet),
+ * coursecat::get_children(), etc. return only visible categories so it is
+ * usually not needed to call this function outside of this class
+ *
+ * @return bool
+ */
+ public function is_uservisible() {
+ return !$this->id || $this->visible ||
+ has_capability('moodle/category:viewhiddencategories',
+ context_coursecat::instance($this->id));
+ }
+
+ /**
+ * Returns all categories visible to the current user
+ *
+ * This is a generic function that returns an array of
+ * (category id => coursecat object) sorted by sortorder
+ *
+ * @see coursecat::get_children()
+ * @see coursecat::get_all_parents()
+ *
+ * @return cacheable_object_array array of coursecat objects
+ */
+ public static function get_all_visible() {
+ global $USER;
+ $coursecatcache = cache::make('core', 'coursecat');
+ $ids = $coursecatcache->get('user'. $USER->id);
+ if ($ids === false) {
+ $all = self::get_all_ids();
+ $parentvisible = $all[0];
+ $rv = array();
+ foreach ($all as $id => $children) {
+ if ($id && in_array($id, $parentvisible) &&
+ ($coursecat = self::get($id, IGNORE_MISSING)) &&
+ (!$coursecat->parent || isset($rv[$coursecat->parent]))) {
+ $rv[$id] = $coursecat;
+ $parentvisible += $children;
+ }
+ }
+ $coursecatcache->set('user'. $USER->id, array_keys($rv));
+ } else {
+ $rv = array();
+ foreach ($ids as $id) {
+ if ($coursecat = self::get($id, IGNORE_MISSING)) {
+ $rv[$id] = $coursecat;
+ }
+ }
+ }
+ return $rv;
+ }
+
+ /**
+ * 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;
+ }
+ // We did not find the entry in cache but it also can mean that tree is not built.
+ // The keys 0 and 'countall' must always be present if tree is built.
+ if ($id !== 0 && $id !== 'countall' && $coursecattreecache->has('countall')) {
+ // Tree was built, it means the non-existing $id was requested.
+ return false;
+ }
+ // 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;
+ }
+ $count++;
+ }
+ $rs->close();
+ if (!$count) {
+ // No categories found.
+ // This may happen after upgrade from 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++;
+ }
+ $all['countall'] = $count;
+ foreach ($all as $key => $children) {
+ $coursecattreecache->set($key, $children);
+ }
+ if (array_key_exists($id, $all)) {
+ return $all[$id];
+ }
+ return false;
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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 course_in_list::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);
+ /*
+ // TODO MDL-38596, this commented code is similar to get_courses_wmanagers()
+ // It bulk-preloads course contacts for all courses BUT it does not check enrolments
+
+ // first build the array of all context ids of the courses and their categories
+ $allcontexts = array();
+ foreach (array_keys($courses) 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;
+ }
+ }
+
+ 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');
+ $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, u.firstname, u.lastname
+ 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."
+ ORDER BY r.sortorder, $sort";
+ $rs = $DB->get_recordset_sql($sql, $params1 + $params2 + $sortparams);
+ foreach($rs as $ra) {
+ foreach ($allcontexts[$ra->contextid] as $id) {
+ $courses[$id]->managers[$ra->raid] = $ra;
+ }
+ }
+ $rs->close();
+ */
+ list($sort, $sortparams) = users_order_by_sql('u');
+ foreach (array_keys($courses) as $id) {
+ $context = context_course::instance($id);
+ $courses[$id]->managers = get_role_users($managerroles, $context, true,
+ 'ra.id AS raid, u.id, u.username, u.firstname, u.lastname, rn.name AS rolecoursealias,
+ r.name AS rolename, r.sortorder, r.id AS roleid, r.shortname AS roleshortname',
+ 'r.sortorder ASC, ' . $sort, false, '', '', '', '', $sortparams);
+ }
+ }
+
+ /**
+ * 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.visible');
+ if (!empty($options['summary'])) {
+ $fields[] = 'c.summary';
+ $fields[] = 'c.summaryformat';
+ } else {
+ $fields[] = $DB->sql_length('c.summary'). ' 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 (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 collatorlib
+ if (count($sortfields) == 1) {
+ $property = key($sortfields);
+ if (in_array($property, array('sortorder', 'id', 'visible', 'parent', 'depth'))) {
+ $sortflag = collatorlib::SORT_NUMERIC;
+ } else if (in_array($property, array('idnumber', 'displayname', 'name', 'shortname', 'fullname'))) {
+ $sortflag = collatorlib::SORT_STRING;
+ } else {
+ $sortflag = collatorlib::SORT_REGULAR;
+ }
+ collatorlib::asort_objects_by_property($records, $property, $sortflag);
+ if ($sortfields[$property] < 0) {
+ $records = array_reverse($records, true);
+ }
+ return;
+ }
+ // sorting by multiple fields
+ 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 array of coursecat 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 coursecat($records[$id]);
+ }
+ }
+ return $rv;
+ }
+
+ /**
+ * 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));
+ }
+
+ /**
+ * 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
+ * @return array
+ */
+ public static function search_courses($search, $options = 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));
+ $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);
+ foreach ($ids as $id) {
+ $courses[$id] = new course_in_list($records[$id]);
+ }
+ }
+ return $courses;
+ }
+
+ $preloadcoursecontacts = !empty($options['coursecontacts']);
+ unset($options['coursecontacts']);
+
+ if (!empty($search['search'])) {
+ // search courses that have specified words in their names/summaries
+ $searchterms = preg_split('|\s+|', trim($search['search']), 0, PREG_SPLIT_NO_EMPTY);
+ $searchterms = array_filter($searchterms, function ($v) { return strlen($v) > 1; } );
+ $courselist = get_courses_search($searchterms, 'c.sortorder ASC', 0, 9999999, $totalcount);
+ 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)";
+ $params = array('tagid' => $search['tagid'], 'itemtype' => 'course');
+ } else {
+ debugging('No criteria is specified while searching courses', DEBUG_DEVELOPER);
+ return array();
+ }
+ $courselist = self::get_course_records($where, $params, $options, true);
+ 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);
+ }
+ $courses = array();
+ foreach ($records as $record) {
+ $courses[$record->id] = new course_in_list($record);
+ }
+ return $courses;
+ }
+
+ /**
+ * Returns number of courses in the search results
+ *
+ * It is recommended to call this function after {@link coursecat::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
+ * @return int
+ */
+ public static function search_courses_count($search, $options = array()) {
+ $coursecatcache = cache::make('core', 'coursecat');
+ $cntcachekey = 'scnt-'. serialize($search);
+ if (($cnt = $coursecatcache->get($cntcachekey)) === false) {
+ self::search_courses($search, $options);
+ $cnt = $coursecatcache->get($cntcachekey);
+ }
+ 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 course_in_list::$summary and/or
+ * course_in_list::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 course_in_list::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
+ * @return array array of instances of course_in_list
+ */
+ 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);
+ foreach ($ids as $id) {
+ $courses[$id] = new course_in_list($records[$id]);
+ }
+ }
+ return $courses;
+ }
+
+ // retrieve list of courses in category
+ $where = 'c.id <> :siteid';
+ $params = array('siteid' => SITEID);
+ if ($recursive) {
+ if ($this->id) {
+ $context = get_category_or_system_context($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 course_in_list 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);
+ }
+ foreach ($list as $record) {
+ $courses[$record->id] = new course_in_list($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) {
+ $this->get_courses($options);
+ $cnt = $coursecatcache->get($cntcachekey);
+ }
+ return $cnt;
+ }
+
+ /**
+ * 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 = context_coursecat::instance($this->id);
+ 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 coursecat::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
+ */
+ 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');
+
+ $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 coursecat($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());
+ }
+
+ // finally delete the category and it's context
+ $DB->delete_records('course_categories', array('id' => $this->id));
+ delete_context(CONTEXT_COURSECAT, $this->id);
+ add_to_log(SITEID, "category", "delete", "index.php", "$this->name (ID $this->id)");
+
+ cache_helper::purge_by_event('changesincoursecat');
+
+ events_trigger('course_category_deleted', $this);
+
+ // 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 = context_coursecat::instance($this->id);
+ 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 = context_coursecat::instance($this->id);
+ 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 coursecat::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();
+ $coursesids = $DB->get_fieldset_select('course', 'id', 'category = :category ORDER BY sortorder ASC', array('category' => $this->id));
+ $context = context_coursecat::instance($this->id);
+
+ if ($children) {
+ foreach ($children as $childcat) {
+ $childcat->change_parent_raw($newparentcat);
+ // Log action.
+ add_to_log(SITEID, "category", "move", "editcategory.php?id=$childcat->id", $childcat->id);
+ }
+ fix_course_sortorder();
+ }
+
+ if ($coursesids) {
+ 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();
+ add_to_log(SITEID, "category", "delete", "index.php", "$this->name (ID $this->id)");
+
+ events_trigger('course_category_deleted', $this);
+
+ 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|coursecat $newparentcat
+ * @return bool
+ */
+ public function can_change_parent($newparentcat) {
+ if (!has_capability('moodle/category:manage', context_coursecat::instance($this->id))) {
+ 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;
+ }
+ return has_capability('moodle/category:manage', get_category_or_system_context($newparentcat->id));
+ }
+
+ /**
+ * 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 coursecat::change_parent()
+ * @see coursecat::update()
+ *
+ * @param coursecat $newparentcat
+ */
+ protected function change_parent_raw(coursecat $newparentcat) {
+ global $DB;
+
+ $context = context_coursecat::instance($this->id);
+
+ $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, array('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 = coursecat::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|coursecat $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();
+ add_to_log(SITEID, "category", "move", "editcategory.php?id=$this->id", $this->id);
+ }
+ }
+
+ /**
+ * 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 coursecat::hide()
+ * @see coursecat::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));
+ $DB->execute("UPDATE {course} SET visibleold = visible WHERE category = ?", array($this->id)); // store visible flag so that we can return to it if we immediately unhide
+ $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');
+ add_to_log(SITEID, "category", "hide", "editcategory.php?id=$this->id", $this->id);
+ }
+ }
+
+ /**
+ * 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 coursecat::show()
+ * @see coursecat::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');
+ add_to_log(SITEID, "category", "show", "editcategory.php?id=$this->id", $this->id);
+ }
+ }
+
+ /**
+ * 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 = context_coursecat::instance($this->id);
+ return format_string($this->name, true, array('context' => $context) + $options);
+ } else {
+ return ''; // TODO 'Top'?
+ }
+ }
+
+ /**
+ * 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)
+ *
+ * coursecat::get(1)->get_parents() == array()
+ * coursecat::get(2)->get_parents() == array(1)
+ * coursecat::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