From 0cbe81112a30a0f0654cca8a62ed458352a8f8bc Mon Sep 17 00:00:00 2001
From: skodak <skodak>
Date: Mon, 16 Jun 2008 14:25:53 +0000
Subject: [PATCH] MDL-14580 rewritten course sorting algorithms

---
 admin/cliupgrade.php          |  14 +-
 admin/index.php               |  11 +-
 backup/restorelib.php         |  41 +---
 course/category.php           |  48 ++---
 course/editcategory.php       |   8 +-
 course/index.php              |  56 ++----
 course/lib.php                |  71 +++----
 course/pending.php            |  14 +-
 enrol/database/enrol.php      |  19 +-
 enrol/imsenterprise/enrol.php |  11 +-
 enrol/ldap/enrol.php          |  23 +--
 lib/accesslib.php             |  17 ++
 lib/datalib.php               | 353 ++++++++++++++++++++--------------
 13 files changed, 313 insertions(+), 373 deletions(-)

diff --git a/admin/cliupgrade.php b/admin/cliupgrade.php
index b39ea248f2..44d86dc660 100644
--- a/admin/cliupgrade.php
+++ b/admin/cliupgrade.php
@@ -1166,17 +1166,9 @@ if ( file_exists(dirname(dirname(__FILE__)) . '/config.php')) {
             $page = page_create_object(PAGE_COURSE_VIEW, $newid);
             blocks_repopulate_page($page); // Return value not checked because you can always edit later
 
-            $cat = new Object();
-            $cat->name = get_string('miscellaneous');
-            $cat->depth = 1;
-            if ($catid = $DB->insert_record('course_categories', $cat)) {
-                // make sure category context exists
-                get_context_instance(CONTEXT_COURSECAT, $catid);
-                mark_context_dirty('/'.SYSCONTEXTID);
-                // do nothing
-            } else {
-                print_error('cannotsetupcategory', 'error');
-            }
+            // create default course category
+            $cat = get_course_category();
+
         } else {
             print_error('cannotsetupsite', 'error');
         }
diff --git a/admin/index.php b/admin/index.php
index d13fc11c9f..125bebbbf6 100644
--- a/admin/index.php
+++ b/admin/index.php
@@ -466,15 +466,8 @@
         $page = page_create_object(PAGE_COURSE_VIEW, $newid);
         blocks_repopulate_page($page); // Return value not checked because you can always edit later
 
-        $cat = new object();
-        $cat->name = get_string('miscellaneous');
-        $cat->depth = 1;
-        if (!$catid = $DB->insert_record('course_categories', $cat)) {
-            print_error('cannotsetupcategory', 'error');
-        }
-        // make sure category context exists
-        get_context_instance(CONTEXT_COURSECAT, $catid);
-        mark_context_dirty('/'.SYSCONTEXTID);
+        // create default course category
+        $cat = get_course_category();
 
         redirect('index.php');
     }
diff --git a/backup/restorelib.php b/backup/restorelib.php
index af3e7d644f..a166b9adff 100644
--- a/backup/restorelib.php
+++ b/backup/restorelib.php
@@ -651,34 +651,13 @@ define('RESTORE_GROUPS_GROUPINGS', 3);
             $category = $DB->get_record("course_categories", array("name"=>$course_header->category->name));
         }
 
-        //If no exists, get category id 1
+        //If no exists, get default category
         if (!$category) {
-            $category = $DB->get_record("course_categories", array("id"=>"1"));
+            $category = get_course_category();
         }
 
-        //If category 1 doesn'exists, lets create the course category (get it from backup file)
-        if (!$category) {
-            $ins_category = new object();
-            $ins_category->name = $course_header->category->name;
-            $ins_category->parent = 0;
-            $ins_category->sortorder = 0;
-            $ins_category->coursecount = 0;
-            $ins_category->visible = 0;            //To avoid interferences with the rest of the site
-            $ins_category->timemodified = time();
-            $newid = $DB->insert_record("course_categories",$ins_category);
-            $category->id = $newid;
-            $category->name = $course_header->category->name;
-        }
-        //If exists, put new category id
-        if ($category) {
-            $course_header->category->id = $category->id;
-            $course_header->category->name = $category->name;
-        //Error, cannot locate category
-        } else {
-            $course_header->category->id = 0;
-            $course_header->category->name = get_string("unknowncategory");
-            $status = false;
-        }
+        $course_header->category->id = $category->id;
+        $course_header->category->name = $category->name;
 
         //Create the course_object
         if ($status) {
@@ -737,16 +716,8 @@ define('RESTORE_GROUPS_GROUPINGS', 3);
                 $course->enrolenddate  += $restore->course_startdateoffset;
             }
             $course->enrolperiod = $course_header->course_enrolperiod;
-            //Calculate sortorder field
-            $sortmax = $DB->get_record_sql('SELECT MAX(sortorder) AS max
-                                              FROM {course}
-                                             WHERE category=?', array($course->category));
-            if (!empty($sortmax->max)) {
-                $course->sortorder = $sortmax->max + 1;
-                unset($sortmax);
-            } else {
-                $course->sortorder = 100;
-            }
+            //Put as last course in category
+            $course->sortorder = $category->sortorder + MAX_COURSES_IN_CATEGORY - 1;
 
             //Now, recode some languages (Moodle 1.5)
             if ($course->lang == 'ma_nt') {
diff --git a/course/category.php b/course/category.php
index 423f64f9e2..61f1c897be 100644
--- a/course/category.php
+++ b/course/category.php
@@ -79,16 +79,12 @@
 
         if ($resort and confirm_sesskey()) {
             if ($courses = get_courses($category->id, "fullname ASC", 'c.id,c.fullname,c.sortorder')) {
-                // move it off the range
-                $count = $DB->get_record_sql('SELECT MAX(sortorder) AS max, 1 FROM {course} WHERE category= ?', array($category->id));
-                $count = $count->max + 100;
-                $DB->begin_sql();
+                $i = 1;
                 foreach ($courses as $course) {
-                    $DB->set_field('course', 'sortorder', $count, array('id'=>$course->id));
-                    $count++;
+                    $DB->set_field('course', 'sortorder', $category->sortorder+$i, array('id'=>$course->id));
+                    $i++;
                 }
-                $DB->commit_sql();
-                fix_course_sortorder($category->id);
+                fix_course_sortorder(); // should not be needed
             }
         }
     }
@@ -205,36 +201,24 @@
 
         if ((!empty($moveup) or !empty($movedown)) and confirm_sesskey()) {
             require_capability('moodle/category:update', $context);
-            $movecourse = NULL;
-            $swapcourse = NULL;
-
-            // ensure the course order has no gaps
-            // and isn't at 0
-            fix_course_sortorder($category->id);
 
-            // we are going to need to know the range
-            $max = $DB->get_record_sql('SELECT MAX(sortorder) AS max, 1 FROM {course} WHERE category=?', array($category->id));
-            $max = $max->max + 100;
+            // ensure the course order has continuous ordering
+            fix_course_sortorder();
+            $swapcourse = NULL;
 
             if (!empty($moveup)) {
-                $movecourse = $DB->get_record('course', array('id'=>$moveup));
-                $swapcourse = $DB->get_record('course', array('category'=>$category->id, 'sortorder'=>($movecourse->sortorder-1)));
+                if ($movecourse = $DB->get_record('course', array('id'=>$moveup))) {
+                    $swapcourse = $DB->get_record('course', array('sortorder'=>$movecourse->sortorder-1));
+                }
             } else {
-                $movecourse = $DB->get_record('course', array('id'=>$movedown));
-                $swapcourse = $DB->get_record('course', array('category'=>$category->id, 'sortorder'=>($movecourse->sortorder+1)));
-            }
-
-            if ($swapcourse and $movecourse) {        // Renumber everything for robustness
-                $DB->begin_sql();
-                if (!(    $DB->set_field("course", "sortorder", $max, array("id"=>$swapcourse->id))
-                       && $DB->set_field("course", "sortorder", $swapcourse->sortorder, array("id"=>$movecourse->id))
-                       && $DB->set_field("course", "sortorder", $movecourse->sortorder, array("id"=>$swapcourse->id))
-                    )) {
-                    notify("Could not update that course!");
+                if ($movecourse = $DB->get_record('course', array('id'=>$movedown))) {
+                    $swapcourse = $DB->get_record('course', array('sortorder'=>$movecourse->sortorder+1));
                 }
-                $DB->commit_sql();
             }
-
+            if ($swapcourse and $movecourse) {
+                $DB->set_field('course', 'sortorder', $swapcourse->sortorder, array('id'=>$movecourse->id));
+                $DB->set_field('course', 'sortorder', $movecourse->sortorder, array('id'=>$swapcourse->id));
+            }
         }
 
     } // End of editing stuff
diff --git a/course/editcategory.php b/course/editcategory.php
index 64970cdc7c..c3390eb196 100644
--- a/course/editcategory.php
+++ b/course/editcategory.php
@@ -58,7 +58,6 @@ if ($mform->is_cancelled()){
     $newcategory = new stdClass();
     $newcategory->name        = $data->name;
     $newcategory->description = $data->description;
-    $newcategory->sortorder   = 999;
     $newcategory->parent      = $data->parent; // if $id = 0, the new category will be a top-level category
 
     if (!empty($data->theme) && !empty($CFG->allowcategorythemes)) {
@@ -66,12 +65,14 @@ if ($mform->is_cancelled()){
         theme_setup();
     }
 
-    if (empty($category) && has_capability('moodle/category:create', $context)) { // Create a new category 
+    if (empty($category) && has_capability('moodle/category:create', $context)) { // Create a new category
+        $newcategory->sortorder   = MAX_COURSES_IN_CATEGORY*MAX_COURSE_CATEGORIES; // put as last category in any parent cat 
         if (!$newcategory->id = $DB->insert_record('course_categories', $newcategory)) {
             notify( "Could not insert the new category '$newcategory->name' ");
         } else {
             $newcategory->context = get_context_instance(CONTEXT_COURSECAT, $newcategory->id);
             mark_context_dirty($newcategory->context->path);
+            fix_course_sortorder();
             redirect('index.php?categoryedit=on');
         }
     } elseif (has_capability('moodle/category:update', $context)) {
@@ -79,7 +80,7 @@ if ($mform->is_cancelled()){
 
         if ($newcategory->parent != $category->parent) {
             $parent_cat = $DB->get_record('course_categories', array('id'=>$newcategory->parent));
-            move_category($newcategory, $parent_cat);
+            move_category($newcategory, $parent_cat); // includes sortorder fix
         }
 
         if (!$DB->update_record('course_categories', $newcategory)) {
@@ -90,7 +91,6 @@ if ($mform->is_cancelled()){
             } else {
                 $redirect_link = 'category.php?id='.$newcategory->id.'&categoryedit=on'; 
             }
-            fix_course_sortorder();
             redirect($redirect_link);
         }
     } 
diff --git a/course/index.php b/course/index.php
index db49319498..f77b1e15c9 100644
--- a/course/index.php
+++ b/course/index.php
@@ -199,61 +199,33 @@
 
     if ((!empty($moveup) or !empty($movedown)) and confirm_sesskey()) {
 
+        fix_course_sortorder();
         $swapcategory = NULL;
-        $movecategory = NULL;
 
         if (!empty($moveup)) {
             if ($movecategory = $DB->get_record('course_categories', array('id'=>$moveup))) {
-                $categories = get_categories($movecategory->parent);
-
-                foreach ($categories as $category) {
-                    if ($category->id == $movecategory->id) {
-                        break;
-                    }
-                    $swapcategory = $category;
+                if ($swapcategory = $DB->get_records_select('course_categories', "sortorder<? AND parent=?", array($movecategory->sortorder, $movecategory->parent), 'sortorder ASC', '*', 0, 1)) {
+                    $swapcategory = reset($swapcategory);
                 }
-                unset($category);
             }
-        }
-        if (!empty($movedown)) {
+        } else {
             if ($movecategory = $DB->get_record('course_categories', array('id'=>$movedown))) {
-                $categories = get_categories($movecategory->parent);
-
-                $choosenext = false;
-                foreach ($categories as $category) {
-                    if ($choosenext) {
-                        $swapcategory = $category;
-                        break;
-                    }
-                    if ($category->id == $movecategory->id) {
-                        $choosenext = true;
-                    }
+                if ($swapcategory = $DB->get_records_select('course_categories', "sortorder>? AND parent=?", array($movecategory->sortorder, $movecategory->parent), 'sortorder ASC', '*', 0, 1)) {
+                    $swapcategory = reset($swapcategory);
                 }
-                unset($category);
             }
         }
-        if ($swapcategory and $movecategory) {        // Renumber everything for robustness
-            $count=0;
-            foreach ($categories as $category) {
-                $count++;
-                if ($category->id == $swapcategory->id) {
-                    $category = $movecategory;
-                } else if ($category->id == $movecategory->id) {
-                    $category = $swapcategory;
-                }
-                if (!$DB->set_field('course_categories', 'sortorder', $count, array('id'=>$category->id))) {
-                    notify('Could not update that category!');
-                }
-            }
-            unset($category);
+        if ($swapcategory and $movecategory) {
+            $DB->set_field('course_categories', 'sortorder', $swapcategory->sortorder, array('id'=>$movecategory->id));
+            $DB->set_field('course_categories', 'sortorder', $movecategory->sortorder, array('id'=>$swapcategory->id));
         }
-    }
 
-/// Find any orphan courses that don't yet have a valid category and set to default
-    fix_coursecategory_orphans();
+        // finally reorder courses
+        fix_course_sortorder();
+    }
 
-/// Should be a no-op 99% of the cases
-    fix_course_sortorder();
+/// This should not be needed anymore
+    //fix_course_sortorder();
 
 /// Print out the categories with all the knobs
 
diff --git a/course/lib.php b/course/lib.php
index 84dcf1adbf..562f9863dc 100644
--- a/course/lib.php
+++ b/course/lib.php
@@ -2854,49 +2854,28 @@ function category_delete_move($category, $newparentid, $showfeedback=true) {
 function move_courses($courseids, $categoryid) {
     global $CFG, $DB;
 
-    if (!empty($courseids)) {
+    if (!empty($courseids) and $category = $DB->get_record('course_categories', array('id'=>$categoryid))) {
+        $courseids = array_reverse($courseids);
+        $i = 1;
 
-            $courseids = array_reverse($courseids);
-
-            foreach ($courseids as $courseid) {
-
-                if (! $course  = $DB->get_record("course", array("id"=>$courseid))) {
-                    notify("Error finding course $courseid");
-                } else {
-                    // figure out a sortorder that we can use in the destination category
-                    $sortorder = $DB->get_field_sql('SELECT MIN(sortorder)-1 AS min
-                                                       FROM {course} WHERE category=?', array($categoryid));
-                    if (is_null($sortorder) || $sortorder === false) {
-                        // the category is empty
-                        // rather than let the db default to 0
-                        // set it to > 100 and avoid extra work in fix_coursesortorder()
-                        $sortorder = 200;
-                    } else if ($sortorder < 10) {
-                        fix_course_sortorder($categoryid);
-                    }
-
-                    $course->category  = $categoryid;
-                    $course->sortorder = $sortorder;
-                    $course->fullname  = $course->fullname;
-                    $course->shortname = $course->shortname;
-                    $course->summary   = $course->summary;
-                    $course->password  = $course->password;
-                    $course->teacher   = $course->teacher;
-                    $course->teachers  = $course->teachers;
-                    $course->student   = $course->student;
-                    $course->students  = $course->students;
-
-                    if (!$DB->update_record('course', $course)) {
-                        notify("An error occurred - course not moved!");
-                    }
+        foreach ($courseids as $courseid) {
+            if (!$course  = $DB->get_record("course", array("id"=>$courseid))) {
+                notify("Error finding course $courseid");
+            } else {
+                $course->category  = $categoryid;
+                $course->sortorder = $category->sortorder + MAX_COURSES_IN_CATEGORY - $i++;
 
-                    $context   = get_context_instance(CONTEXT_COURSE, $course->id);
-                    $newparent = get_context_instance(CONTEXT_COURSECAT, $course->category);
-                    context_moved($context, $newparent);
+                if (!$DB->update_record('course', $course)) {
+                    notify("An error occurred - course not moved!");
                 }
+
+                $context   = get_context_instance(CONTEXT_COURSE, $course->id);
+                $newparent = get_context_instance(CONTEXT_COURSECAT, $course->category);
+                context_moved($context, $newparent);
             }
-            fix_course_sortorder();
         }
+        fix_course_sortorder();
+    }
     return true;
 }
 
@@ -2913,7 +2892,9 @@ function move_category ($category, $newparentcat) {
         if (!$DB->set_field('course_categories', 'parent', 0, array('id'=>$category->id))) {
             return false;
         }
+
         $newparent = get_context_instance(CONTEXT_SYSTEM);
+
     } else {
         if (!$DB->set_field('course_categories', 'parent', $newparentcat->id, array('id'=>$category->id))) {
             return false;
@@ -2923,8 +2904,10 @@ function move_category ($category, $newparentcat) {
 
     context_moved($context, $newparent);
 
-    // The most effective thing would be to find the common parent,
-    // until then, do it sitewide...
+    // now make it last in new category 
+    $DB->set_field('course_categories', 'sortorder', MAX_COURSES_IN_CATEGORY*MAX_COURSE_CATEGORIES, array('id'=>$category->id));
+
+    // and fix the sortorders
     fix_course_sortorder();
 
     return true;
@@ -2990,12 +2973,8 @@ function create_course($data) {
 
     $data->timecreated = time();
 
-    // place at beginning of category
-    fix_course_sortorder();
-    $data->sortorder = $DB->get_field_sql("SELECT MIN(sortorder)-1 FROM {course} WHERE category=?", array($data->category));
-    if (empty($data->sortorder)) {
-        $data->sortorder = 100;
-    }
+    // place at beginning of any category
+    $data->sortorder = 0;
 
     if ($newcourseid = $DB->insert_record('course', $data)) {  // Set up new course
 
diff --git a/course/pending.php b/course/pending.php
index 7564f7a541..11de7e4e6a 100644
--- a/course/pending.php
+++ b/course/pending.php
@@ -17,19 +17,10 @@
     if (!empty($approve) and confirm_sesskey()) {
         if ($course = $DB->get_record("course_request", array("id"=>$approve))) {
 
-            // place at beginning of category
-            fix_course_sortorder();
-
-            if (empty($CFG->defaultrequestcategory) or !$DB->record_exists('course_categories', array('id'=>$CFG->defaultrequestcategory))) {
-                // default to first top level directory, hacky but means things don't break
-                $CFG->defaultrequestcategory = $DB->get_field('course_categories', 'id', array('parent'=>'0'));
-            }
+            $category = get_course_category($CFG->defaultrequestcategory);
 
             $course->category = $CFG->defaultrequestcategory;
-            $course->sortorder = $DB->get_field_sql("SELECT min(sortorder)-1 FROM {course} WHERE category=?", array($course->category));
-            if (empty($course->sortorder)) {
-                $course->sortorder = 1000;
-            }
+            $course->sortorder = $category->sortorder; // place as the first in category 
             $course->requested = 1;
             unset($course->reason);
             unset($course->id);
@@ -54,6 +45,7 @@
                 }
                 $DB->delete_records('course_request', array('id'=>$approve));
                 $success = 1;
+                fix_course_sortorder();
             }
             if (!empty($success)) {
                 $user = $DB->get_record('user', array('id'=>$teacherid));
diff --git a/enrol/database/enrol.php b/enrol/database/enrol.php
index f848a0ff6e..3cd6acf15f 100644
--- a/enrol/database/enrol.php
+++ b/enrol/database/enrol.php
@@ -554,7 +554,7 @@ function create_course ($course,$skip_fix_course_sortorder=0){
     global $CFG, $DB;
 
     // define a template
-    if(!empty($CFG->enrol_db_template)){
+    if (!empty($CFG->enrol_db_template)){
         $template = $DB->get_record("course", array('shortname'=>$CFG->enrol_db_template));
         $template = (array)$template;
     } else {
@@ -585,14 +585,10 @@ function create_course ($course,$skip_fix_course_sortorder=0){
         }
     }
 
-    $course->category = 1;     // the misc 'catch-all' category
-    if (!empty($CFG->enrol_db_category)){ //category = 0 or undef will break moodle
-        $course->category = $CFG->enrol_db_category;
-    }
+    $category = get_course_category($CFG->enrol_db_category);
 
-    // define the sortorder
-    $sort = $DB->get_field_sql('SELECT COALESCE(MAX(sortorder)+1, 100) AS max FROM {course} WHERE category= ?', array($course->category));
-    $course->sortorder = $sort;
+    // put at the end of category
+    $course->sortorder = $category->sortorder + MAX_COURSES_IN_CATEGORY - 1;
 
     // override with local data
     $course->startdate   = time() + 3600 * 24;
@@ -608,15 +604,14 @@ function create_course ($course,$skip_fix_course_sortorder=0){
 
     // store it and log
     if ($newcourseid = $DB->insert_record("course", $course)) {  // Set up new course
-        $section = NULL;
-        $section->course = $newcourseid;   // Create a default section.
+        $section = new object();
+        $section->course  = $newcourseid;   // Create a default section.
         $section->section = 0;
         $section->id = $DB->insert_record("course_sections", $section);
         $page = page_create_object(PAGE_COURSE_VIEW, $newcourseid);
         blocks_repopulate_page($page); // Return value no
 
-
-        if(!$skip_fix_course_sortorder){
+        if (!$skip_fix_course_sortorder){
             fix_course_sortorder();
         }
         add_to_log($newcourseid, "course", "new", "view.php?id=$newcourseid", "enrol/database auto-creation");
diff --git a/enrol/imsenterprise/enrol.php b/enrol/imsenterprise/enrol.php
index 325dce251d..1c26cfc7dd 100644
--- a/enrol/imsenterprise/enrol.php
+++ b/enrol/imsenterprise/enrol.php
@@ -536,15 +536,8 @@ function process_group_tag($tagcontents){
                 $course->startdate = time();
                 $course->numsections = 1;
                 // Choose a sort order that puts us at the start of the list!
-                $sortinfo = $DB->get_record_sql('SELECT MIN(sortorder) AS min, MAX(sortorder) AS max FROM {course} WHERE category<>0');
-                if (is_object($sortinfo)) { // no courses?
-                    $max   = $sortinfo->max;
-                    $min   = $sortinfo->min;
-                    unset($sortinfo);
-                    $course->sortorder = $min - 1;
-                }else{
-                    $course->sortorder = 1000;
-                }
+                $course->sortorder = 0;
+
                 if($course->id = $DB->insert_record('course', $course)){
 
                     // Setup the blocks
diff --git a/enrol/ldap/enrol.php b/enrol/ldap/enrol.php
index 23c57d2d5f..bdab28a084 100755
--- a/enrol/ldap/enrol.php
+++ b/enrol/ldap/enrol.php
@@ -550,7 +550,7 @@ function create_course ($course_ext,$skip_fix_course_sortorder=0){
     global $CFG, $DB;
 
     // override defaults with template course
-    if(!empty($CFG->enrol_ldap_template)){
+    if (!empty($CFG->enrol_ldap_template)){
         $course = $DB->get_record("course", array('shortname'=>$CFG->enrol_ldap_template));
         unset($course->id); // so we are clear to reinsert the record
         unset($course->sortorder);
@@ -580,19 +580,11 @@ function create_course ($course_ext,$skip_fix_course_sortorder=0){
     $course->summary = empty($CFG->enrol_ldap_course_summary) || empty($course_ext[$CFG->enrol_ldap_course_summary][0])
         ? ''
         : $course_ext[$CFG->enrol_ldap_course_summary][0];
-    
-    if(!empty($CFG->enrol_ldap_category)){ // optional ... but ensure it is set!
-        $course->category = $CFG->enrol_ldap_category;
-    } 
-    if ($course->category == 0){ // must be avoided as it'll break moodle
-        $course->category = 1; // the misc 'catch-all' category
-    }
 
-    // define the sortorder (yuck)
-    $sort = $DB->get_record_sql('SELECT MAX(sortorder) AS max, 1 FROM {course} WHERE category=?', array($course->category));
-    $sort = $sort->max;
-    $sort++;
-    $course->sortorder = $sort; 
+    $category = get_course_category($CFG->enrol_db_category);
+
+    // put at the end of category
+    $course->sortorder = $category->sortorder + MAX_COURSES_IN_CATEGORY - 1;
 
     // override with local data
     $course->startdate = time();
@@ -602,14 +594,13 @@ function create_course ($course_ext,$skip_fix_course_sortorder=0){
     // store it and log
     if ($newcourseid = $DB->insert_record("course", $course)) {  // Set up new course
         $section = new object();
-        $section->course = $newcourseid;   // Create a default section.
+        $section->course  = $newcourseid;   // Create a default section.
         $section->section = 0;
         $section->id = $DB->insert_record("course_sections", $section);
         $page = page_create_object(PAGE_COURSE_VIEW, $newcourseid);
         blocks_repopulate_page($page); // Return value no
 
-
-        if(!$skip_fix_course_sortorder){ 
+        if (!$skip_fix_course_sortorder){ 
             fix_course_sortorder(); 
         }
         add_to_log($newcourseid, "course", "new", "view.php?id=$newcourseid", "enrol/ldap auto-creation");
diff --git a/lib/accesslib.php b/lib/accesslib.php
index 4f6eeb5578..43ff108f83 100755
--- a/lib/accesslib.php
+++ b/lib/accesslib.php
@@ -5118,6 +5118,23 @@ function component_level_changed($cap, $comp, $contextlevel) {
     return ($cap->component != $comp || $cap->contextlevel != $contextlevel);
 }
 
+/**
+ * Rebuild all related context depth and path caches
+ * @param array $fixcontexts array of contexts
+ */
+function rebuild_contexts(array $fixcontexts) {
+    global $DB;
+
+    foreach ($fixcontexts as $context) {
+        if ($context->path) {
+            mark_context_dirty($context->path);
+        }
+        $DB->set_field_select('context', 'depth', 0, "path LIKE '%/$context->id/%'");
+        $DB->set_field('context', 'depth', 0, array('id'=>$context->id));
+    }
+    build_context_path(false);
+}
+
 /**
  * Populate context.path and context.depth where missing.
  * @param bool $force force a complete rebuild of the path and depth fields.
diff --git a/lib/datalib.php b/lib/datalib.php
index 8c1d76b3b5..e960298890 100644
--- a/lib/datalib.php
+++ b/lib/datalib.php
@@ -11,6 +11,9 @@
  * @package moodlecore
  */
 
+define('MAX_COURSES_IN_CATEGORY', 10000); // MAX_COURSES_IN_CATEGORY * MAX_COURSE_CATEGORIES must not be more than max integer!  
+define('MAX_COURSE_CATEGORIES', 10000);
+
 /**
  * Sets up global $DB moodle_database instance
  * @return void
@@ -1272,185 +1275,243 @@ function get_all_subcategories($catid) {
     return $subcats;
 }
 
-
 /**
-* This recursive function makes sure that the courseorder is consecutive
-*
-* @param    type description
-*
-* $n is the starting point, offered only for compatilibity -- will be ignored!
-* $safe (bool) prevents it from assuming category-sortorder is unique, used to upgrade
-*       safely from 1.4 to 1.5
-*/
-function fix_course_sortorder($categoryid=0, $n=0, $safe=0, $depth=0, $path='') {
-    global $CFG, $DB;
+ * Return specified category, default if given does not exist
+ * @param int $catid course category id
+ * @return object caregory
+ */
+function get_course_category($catid=0) {
+    global $DB;
+
+    $category = false;
+
+    if (!empty($catid)) {
+        $category = $DB->get_record('course_categories', array('id'=>$catid));
+    }
 
-    $count = 0;
-
-    $catgap    = 1000; // "standard" category gap
-    $tolerance = 200;  // how "close" categories can get
-
-    if ($categoryid > 0){
-        // update depth and path
-        $cat   = $DB->get_record('course_categories', array('id'=>$categoryid));
-        if ($cat->parent == 0) {
-            $depth = 0;
-            $path  = '';
-        } else if ($depth == 0 ) { // doesn't make sense; get from DB
-            // this is only called if the $depth parameter looks dodgy
-            $parent = $DB->get_record('course_categories', array('id'=>$cat->parent));
-            $path  = $parent->path;
-            $depth = $parent->depth;
+    if (!$category) {
+        // the first category is considered default for now
+        if ($category = $DB->get_records('course_categories', null, 'sortorder', '*', 0, 1)) {
+            $category = reset($category);
+
+        } else {
+            $cat = new object();
+            $cat->name         = get_string('miscellaneous');
+            $cat->depth        = 1;
+            $cat->sortorder    = MAX_COURSES_IN_CATEGORY;
+            $cat->timemodified = time();
+            if (!$catid = $DB->insert_record('course_categories', $cat)) {
+                print_error('cannotsetupcategory', 'error');
+            }
+            // make sure category context exists
+            get_context_instance(CONTEXT_COURSECAT, $catid);
+            mark_context_dirty('/'.SYSCONTEXTID);
+            $category = $DB->get_record('course_categories', array('id'=>$catid));
         }
-        $path  = $path . '/' . $categoryid;
-        $depth = $depth + 1;
+    }
+
+    return $category;
+}
+
+/**
+ * Fixes course category and course sortorder, also verifies category and course parents and paths.
+ * (circular references are not fixed) 
+ */
+function fix_course_sortorder() {
+    global $DB, $SITE;
+
+    //WARNING: this is PHP5 only code!
+
+    if ($unsorted = $DB->get_records('course_categories', array('sortorder'=>0))) {
+        //move all categories that are not sorted yet to the end
+        $DB->set_field('course_categories', 'sortorder', MAX_COURSES_IN_CATEGORY*MAX_COURSE_CATEGORIES, array('sortorder'=>0));
+    }
 
-        if ($cat->path !== $path) {
-            $DB->set_field('course_categories', 'path',  $path,  array('id'=>$categoryid));
+    $allcats = $DB->get_records('course_categories', null, 'sortorder, id', 'id, sortorder, parent, depth, path');
+    $topcats    = array();
+    $brokencats = array();
+    foreach ($allcats as $cat) {
+        $sortorder = (int)$cat->sortorder;
+        if (!$cat->parent) {
+            while(isset($topcats[$sortorder])) {
+                $sortorder++;
+            }
+            $topcats[$sortorder] = $cat;
+            continue;
         }
-        if ($cat->depth != $depth) {
-            $DB->set_field('course_categories', 'depth', $depth, array('id'=>$categoryid));
+        if (!isset($allcats[$cat->parent])) {
+            $brokencats[] = $cat;
+            continue;
         }
+        if (!isset($allcats[$cat->parent]->children)) {
+            $allcats[$cat->parent]->children = array();
+        }
+        while(isset($allcats[$cat->parent]->children[$sortorder])) {
+            $sortorder++;
+        }
+        $allcats[$cat->parent]->children[$sortorder] = $cat;
     }
+    unset($allcats);
 
-    // get some basic info about courses in the category
-    $info = $DB->get_record_sql("SELECT MIN(sortorder) AS min,
-                                        MAX(sortorder) AS max,
-                                        COUNT(sortorder) AS count
-                                   FROM {course}
-                                  WHERE category=?", array($categoryid));
-    if ($info) { // no courses?
-        $max   = $info->max;
-        $count = $info->count;
-        $min   = $info->min;
-        unset($info);
+    // add broken cats to category tree
+    if ($brokencats) {
+        $defaultcat = reset($topcats);
+        foreach ($brokencats as $cat) {
+            $topcats[] = $cat;
+        } 
     }
 
-    if ($categoryid > 0 && $n==0) { // only passed category so don't shift it
-        $n = $min;
+    // now walk recursively the tree and fix any problems found
+    $sortorder = 0;
+    $fixcontexts = array();
+    _fix_course_cats($topcats, $sortorder, 0, 0, '', $fixcontexts);
+
+    // detect if there are "multiple" frontpage courses and fix them if needed
+    $frontcourses = $DB->get_records('course', array('category'=>0), 'id');
+    if (count($frontcourses) > 1) {
+        if (isset($frontcourses[SITEID])) {
+            $frontcourse = $frontcourses[SITEID];
+            unset($frontcourses[SITEID]);
+        } else {
+            $frontcourse = array_shift($frontcourses);
+        }
+        $defaultcat = reset($topcats);
+        foreach ($frontcourses as $course) {
+            $DB->set_field('course', 'category', $defaultcat->id, array('id'=>$course->id));
+            $context = get_context_instance(CONTEXT_COURSE, $course->id);
+            $fixcontexts[$context->id] = $context;
+        }
+        unset($frontcourses);
+    } else {
+        $frontcourse = reset($frontcourses);
     }
 
-    // $hasgap flag indicates whether there's a gap in the sequence
-    $hasgap    = false;
-    if ($max-$min+1 != $count) {
-        $hasgap = true;
+    // now fix the paths and depths in context table if needed
+    if ($fixcontexts) {
+        rebuild_contexts($fixcontexts);
     }
 
-    // $mustshift indicates whether the sequence must be shifted to
-    // meet its range
-    $mustshift = false;
-    if ($min < $n+$tolerance || $min > $n+$tolerance+$catgap ) {
-        $mustshift = true;
+    // release memory
+    unset($topcats);
+    unset($brokencats);
+    unset($fixcontexts);
+
+    // fix frontpage course sortorder
+    if ($frontcourse->sortorder != 1) {
+        $DB->set_field('course', 'sortorder', 1, array('id'=>$frontcourse->id));
     }
 
-    // actually sort only if there are courses,
-    // and we meet one ofthe triggers:
-    //  - safe flag
-    //  - they are not in a continuos block
-    //  - they are too close to the 'bottom'
-    if ($count && ( $safe || $hasgap || $mustshift ) ) {
-        // special, optimized case where all we need is to shift
-        if ( $mustshift && !$safe && !$hasgap) {
-            $shift = $n + $catgap - $min;
-            if ($shift < $count) {
-                $shift = $count + $catgap;
-            }
-            // UPDATE course SET sortorder=sortorder+$shift
-            $DB->execute("UPDATE {course}
-                             SET sortorder=sortorder+?
-                           WHERE category=?", array($shift, $categoryid));
-            $n = $n + $catgap + $count;
-
-        } else { // do it slowly
-            $n = $n + $catgap;
-            // if the new sequence overlaps the current sequence, lack of transactions
-            // will stop us -- shift things aside for a moment...
-            if ($safe || ($n >= $min && $n+$count+1 < $min && $CFG->dbfamily==='mysql')) {
-                $shift = $max + $n + 1000;
-                $DB->execute("UPDATE {course}
-                                 SET sortorder=sortorder+?
-                               WHERE category=?", array($shift, $categoryid));
-            }
+    // now fix the course counts in category records if needed
+    $sql = "SELECT cc.id, cc.coursecount, COUNT(c.id) AS newcount
+              FROM {course_categories} cc
+              LEFT JOIN {course} c ON c.category = cc.id
+          GROUP BY cc.id, cc.coursecount
+            HAVING cc.coursecount <> COUNT(c.id)";
 
-            $courses = get_courses($categoryid, 'c.sortorder ASC', 'c.id,c.sortorder');
-            $DB->begin_sql();
-            $tx = true; // transaction sanity
-            foreach ($courses as $course) {
-                if ($tx && $course->sortorder != $n ) { // save db traffic
-                    $tx = $tx && $DB->set_field('course', 'sortorder', $n, array('id'=>$course->id));
-                }
-                $n++;
-            }
-            if ($tx) {
-                $DB->commit_sql();
-            } else {
-                $DB->rollback_sql();
-                if (!$safe) {
-                    // if we failed when called with !safe, try
-                    // to recover calling self with safe=true
-                    return fix_course_sortorder($categoryid, $n, true, $depth, $path);
-                }
-            }
-        }
+    if ($updatecounts = $DB->get_records_sql($sql)) {
+        foreach ($updatecounts as $cat) {
+            $cat->coursecount = $cat->newcount;
+            unset($cat->newcount);
+            $DB->update_record_raw('course_categories', $cat, true);
+        } 
     }
-    $DB->set_field('course_categories', 'coursecount', $count, array('id'=>$categoryid));
 
-    // $n could need updating
-    $max = $DB->get_field_sql("SELECT MAX(sortorder) FROM {course} WHERE category=?", array($categoryid));
-    if ($max > $n) {
-        $n = $max;
+    // now make sure that sortorders in course table are withing the category sortorder ranges
+    $sql = "SELECT cc.id, cc.sortorder
+              FROM {course_categories} cc
+              JOIN {course} c ON c.category = cc.id
+             WHERE c.sortorder < cc.sortorder OR c.sortorder > cc.sortorder + ".MAX_COURSES_IN_CATEGORY;
+
+    if ($fixcategories = $DB->get_records_sql($sql)) {
+        //fix the course sortorder ranges
+        foreach ($fixcategories as $cat) {
+            $sql = "UPDATE {course}
+                       SET sortorder = (sortorder % ".MAX_COURSES_IN_CATEGORY.") + ?
+                     WHERE category = ?";
+            $DB->execute($sql, array($cat->sortorder, $cat->id));
+        }
     }
+    unset($fixcategories);
+
+    // categories having courses with sortorder duplicates or having gaps in sortorder
+    $sql = "SELECT DISTINCT c1.category AS id , cc.sortorder
+              FROM {course} c1
+              JOIN {course} c2 ON c1.sortorder = c2.sortorder
+              JOIN {course_categories} cc ON (c1.category = cc.id)
+             WHERE c1.id <> c2.id";
+    $fixcategories = $DB->get_records_sql($sql);
+
+    $sql = "SELECT cc.id, cc.sortorder, cc.coursecount, MAX(c.sortorder) AS maxsort, MIN(c.sortorder) AS minsort
+              FROM {course_categories} cc
+              JOIN {course} c ON c.category = cc.id
+          GROUP BY cc.id, cc.sortorder, cc.coursecount
+            HAVING (MAX(c.sortorder) <>  cc.sortorder + cc.coursecount) OR (MIN(c.sortorder) <>  cc.sortorder + 1)";
+    $gapcategories = $DB->get_records_sql($sql);
+
+    foreach ($gapcategories as $cat) {
+        if (isset($fixcategories[$cat->id])) {
+            // duplicates detected already
+
+        } else if ($cat->minsort == $cat->sortorder and $cat->maxsort == $cat->sortorder + $cat->coursecount - 1) {
+            // easy - new course inserted with sortorder 0, the rest is ok
+            $sql = "UPDATE {course}
+                       SET sortorder = sortorder + 1
+                     WHERE category = ?";
+            $DB->execute($sql, array($cat->id));
 
-    if ($categories = get_categories($categoryid)) {
-        foreach ($categories as $category) {
-            $n = fix_course_sortorder($category->id, $n, $safe, $depth, $path);
+        } else {
+            // it needs full resorting
+            $fixcategories[$cat->id] = $cat;
         }
     }
+    unset($gapcategories);
 
-    return $n+1;
+    // fix course sortorders in problematic categories only
+    foreach ($fixcategories as $cat) {
+        $i = 1;
+        $courses = $DB->get_records('course', array('category'=>$cat->id), 'sortorder ASC, id DESC', 'id, sortorder');
+        foreach ($courses as $course) {
+            if ($course->sortorder != $cat->sortorder + $i) {
+                $course->sortorder = $cat->sortorder + $i;
+                $DB->update_record_raw('course', $course, true); 
+            }
+            $i++;
+        }
+    }
 }
 
 /**
- * Ensure all courses have a valid course category
- * useful if a category has been removed manually
- **/
-function fix_coursecategory_orphans() {
+ * Internal recursive category verification function, do not use directly!
+ */
+function _fix_course_cats($children, &$sortorder, $parent, $depth, $path, &$fixcontexts) {
     global $DB;
 
-    // Note: the handling of sortorder here is arguably
-    // open to race conditions. Hard to fix here, unlikely
-    // to hit anyone in production.
-
-    $sql = "SELECT c.id, c.category, c.shortname
-              FROM {course} c
-              LEFT OUTER JOIN {course_categories} cc ON c.category=cc.id
-             WHERE cc.id IS NULL AND c.id <> " . SITEID;
-
-    if (!$rs = $DB->get_recordset_sql($sql)) {
-        return;
-    }
-
-    if ($rs->valid()) { // we have some orphans
-
-        // the "default" category is the lowest numbered...
-        $default   = $DB->get_field_sql("SELECT MIN(id)
-                                           FROM {course_categories}");
-        $sortorder = $DB->get_field_sql("SELECT MAX(sortorder)
-                                           FROM {course}
-                                          WHERE category=?", array($default));
+    $depth++;
 
+    foreach ($children as $cat) {
+        $sortorder = $sortorder + MAX_COURSES_IN_CATEGORY;
+        $update = false;
+        if ($parent != $cat->parent or $depth != $cat->depth or $path.'/'.$cat->id != $cat->path) {
+            $cat->parent = $parent;
+            $cat->depth  = $depth;
+            $cat->path   = $path.'/'.$cat->id;
+            $update = true;
 
-        $DB->begin_sql();
-        foreach ($rs as $course) {
-            if (!$DB->set_field('course', 'category',  $default, array('id'=>$course->id))
-              or !$DB->set_field('course', 'sortorder', ++$sortorder, array('id'=>$course->id))) {
-                $DB->rollback_sql();
-                return;
-            }
+            // make sure context caches are rebuild and dirty contexts marked
+            $context = get_context_instance(CONTEXT_COURSECAT, $cat->id);
+            $fixcontexts[$context->id] = $context;
+        }
+        if ($cat->sortorder != $sortorder) {
+            $cat->sortorder = $sortorder;
+            $update = true;
+        }
+        if ($update) {
+            $DB->update_record('course_categories', $cat, true);
+        }
+        if (isset($cat->children)) {
+            _fix_course_cats($cat->children, $sortorder, $cat->id, $cat->depth, $cat->path, $fixcontexts);
         }
-        $DB->commit_sql();
     }
-    $rs->close();
 }
 
 /**
-- 
2.39.5