Merge branch 'MDL-41914-weekly' of git://github.com/jleyva/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 7 Oct 2013 14:06:32 +0000 (16:06 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 7 Oct 2013 14:06:32 +0000 (16:06 +0200)
89 files changed:
admin/roles/assign.php
admin/roles/permissions.php
admin/settings/courses.php
admin/tool/behat/tests/behat/basic_actions.feature
admin/tool/behat/tests/behat/data_generators.feature
admin/tool/behat/tests/behat/manipulate_forms.feature
admin/tool/dbtransfer/locallib.php
backup/cc/cc_lib/gral_lib/cssparser.php
badges/ajax.php
badges/assertion.php
badges/backpackconnect.php
course/ajax/management.php [new file with mode: 0644]
course/category.php
course/classes/deletecategory_form.php [new file with mode: 0644]
course/classes/editcategory_form.php [new file with mode: 0644]
course/classes/management/helper.php [new file with mode: 0644]
course/classes/management_renderer.php [new file with mode: 0644]
course/delete.php
course/delete_category_form.php
course/edit.php
course/editcategory.php
course/editcategory_form.php
course/format/lib.php
course/format/singleactivity/lib.php
course/format/singleactivity/settingslib.php
course/format/singleactivity/styles.css
course/lib.php
course/manage.php
course/management.php [new file with mode: 0644]
course/moodleform_mod.php
course/renderer.php
course/request.php
course/search.php
course/tests/behat/behat_course.php
course/tests/behat/category_change_visibility.feature [new file with mode: 0644]
course/tests/behat/category_management.feature [new file with mode: 0644]
course/tests/behat/category_resort.feature [new file with mode: 0644]
course/tests/behat/course_category_management_listing.feature [new file with mode: 0644]
course/tests/behat/course_change_visibility.feature [new file with mode: 0644]
course/tests/behat/course_resort.feature [new file with mode: 0644]
course/tests/courselib_test.php
course/tests/fixtures/course_capability_assignment.php [new file with mode: 0644]
course/tests/management_helper_test.php [new file with mode: 0644]
course/yui/build/moodle-course-management/moodle-course-management-debug.js [new file with mode: 0644]
course/yui/build/moodle-course-management/moodle-course-management-min.js [new file with mode: 0644]
course/yui/build/moodle-course-management/moodle-course-management.js [new file with mode: 0644]
course/yui/src/management/build.json [new file with mode: 0644]
course/yui/src/management/js/category.js [new file with mode: 0644]
course/yui/src/management/js/console.js [new file with mode: 0644]
course/yui/src/management/js/course.js [new file with mode: 0644]
course/yui/src/management/js/dd.js [new file with mode: 0644]
course/yui/src/management/js/item.js [new file with mode: 0644]
course/yui/src/management/meta/management.json [new file with mode: 0644]
filter/manage.php
lang/en/admin.php
lang/en/error.php
lang/en/moodle.php
lib/classes/session/database.php
lib/classes/shutdown_manager.php [new file with mode: 0644]
lib/classes/text.php
lib/coursecatlib.php
lib/dml/moodle_database.php
lib/filelib.php
lib/filestorage/file_storage.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputrenderers.php
lib/setup.php
lib/tests/behat/behat_general.php
lib/tests/component_test.php
lib/tests/coursecatlib_test.php
lib/tests/text_test.php
lib/upgrade.txt
lib/upgradelib.php
lib/weblib.php
mod/folder/mod_form.php
mod/forum/tests/behat/edit_post_student.feature
mod/quiz/renderer.php
mod/quiz/view.php
repository/tests/behat/delete_files.feature
theme/base/style/core.css
theme/base/style/course.css
theme/base/style/filemanager.css
theme/bootstrapbase/less/moodle/bootstrapoverride.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/less/moodle/filemanager.less
theme/bootstrapbase/less/moodle/responsive.less
theme/bootstrapbase/style/moodle.css

index cce2334..6fa632e 100644 (file)
@@ -29,6 +29,7 @@ define("MAX_USERS_TO_LIST_PER_ROLE", 10);
 
 $contextid = required_param('contextid', PARAM_INT);
 $roleid    = optional_param('roleid', 0, PARAM_INT);
+$returnto  = optional_param('return', null, PARAM_ALPHANUMEXT);
 
 list($context, $course, $cm) = get_context_info_array($contextid);
 
@@ -181,6 +182,9 @@ if ($roleid) {
 
     // Print the form.
     $assignurl = new moodle_url($PAGE->url, array('roleid'=>$roleid));
+    if ($returnto !== null) {
+        $assignurl->param('return', $returnto);
+    }
 ?>
 <form id="assignform" method="post" action="<?php echo $assignurl ?>"><div>
   <input type="hidden" name="sesskey" value="<?php echo sesskey() ?>" />
@@ -225,10 +229,17 @@ if ($roleid) {
     // Print a form to swap roles, and a link back to the all roles list.
     echo '<div class="backlink">';
 
-    $select = new single_select($PAGE->url, 'roleid', $nameswithcounts, $roleid, null);
+    $newroleurl = new moodle_url($PAGE->url);
+    if ($returnto !== null) {
+        $newroleurl->param('return', $returnto);
+    }
+    $select = new single_select($newroleurl, 'roleid', $nameswithcounts, $roleid, null);
     $select->label = get_string('assignanotherrole', 'core_role');
     echo $OUTPUT->render($select);
     $backurl = new moodle_url('/admin/roles/assign.php', array('contextid' => $contextid));
+    if ($returnto !== null) {
+        $backurl->param('return', $returnto);
+    }
     echo '<p><a href="' . $backurl->out() . '">' . get_string('backtoallroles', 'core_role') . '</a></p>';
     echo '</div>';
 
@@ -266,6 +277,9 @@ if ($roleid) {
             }
         } else if ($assigncounts[$roleid] > MAX_USERS_TO_LIST_PER_ROLE) {
             $assignurl = new moodle_url($PAGE->url, array('roleid'=>$roleid));
+            if ($returnto !== null) {
+                $assignurl->param('return', $returnto);
+            }
             $roleholdernames[$roleid] = '<a href="'.$assignurl.'">'.$strmorethanmax.'</a>';
         } else {
             $roleholdernames[$roleid] = '';
@@ -286,6 +300,9 @@ if ($roleid) {
     foreach ($assignableroles as $roleid => $rolename) {
         $description = format_string($DB->get_field('role', 'description', array('id'=>$roleid)));
         $assignurl = new moodle_url($PAGE->url, array('roleid'=>$roleid));
+        if ($returnto !== null) {
+            $assignurl->param('return', $returnto);
+        }
         $row = array('<a href="'.$assignurl.'">'.$rolename.'</a>',
                 $description, $assigncounts[$roleid]);
         if ($showroleholders) {
@@ -297,8 +314,15 @@ if ($roleid) {
     echo html_writer::table($table);
 
     if ($context->contextlevel > CONTEXT_USER) {
+
+        if ($context->contextlevel === CONTEXT_COURSECAT && $returnto === 'management') {
+            $url = new moodle_url('/course/management.php', array('categoryid' => $context->instanceid));
+        } else {
+            $url = $context->get_url();
+        }
+
         echo html_writer::start_tag('div', array('class'=>'backlink'));
-        echo html_writer::tag('a', get_string('backto', '', $contextname), array('href'=>$context->get_url()));
+        echo html_writer::tag('a', get_string('backto', '', $contextname), array('href' => $url));
         echo html_writer::end_tag('div');
     }
 }
index cfff956..8af137a 100644 (file)
@@ -33,6 +33,7 @@ $prevent    = optional_param('prevent', 0, PARAM_BOOL);
 $allow      = optional_param('allow', 0, PARAM_BOOL);
 $unprohibit = optional_param('unprohibit', 0, PARAM_BOOL);
 $prohibit   = optional_param('prohibit', 0, PARAM_BOOL);
+$return     = optional_param('return', null, PARAM_ALPHANUMEXT);
 
 list($context, $course, $cm) = get_context_info_array($contextid);
 
@@ -201,8 +202,15 @@ echo $OUTPUT->box_end();
 
 
 if ($context->contextlevel > CONTEXT_USER) {
+
+    if ($context->contextlevel === CONTEXT_COURSECAT && $return === 'management') {
+        $url = new moodle_url('/course/management.php', array('categoryid' => $context->instanceid));
+    } else {
+        $url = $context->get_url();
+    }
+
     echo html_writer::start_tag('div', array('class'=>'backlink'));
-    echo html_writer::tag('a', get_string('backto', '', $contextname), array('href'=>$context->get_url()));
+    echo html_writer::tag('a', get_string('backto', '', $contextname), array('href' => $url));
     echo html_writer::end_tag('div');
 }
 
index 4005c7c..d69b8e2 100644 (file)
@@ -1,20 +1,51 @@
 <?php
-
-// This file defines settingpages and externalpages under the "courses" category
-
-if ($hassiteconfig
- or has_capability('moodle/backup:backupcourse', $systemcontext)
- or has_capability('moodle/category:manage', $systemcontext)
- or has_capability('moodle/course:create', $systemcontext)
- or has_capability('moodle/site:approvecourse', $systemcontext)) { // speedup for non-admins, add all caps used on this page
-
-    $ADMIN->add('courses', new admin_externalpage('coursemgmt', new lang_string('coursemgmt', 'admin'), $CFG->wwwroot . '/course/manage.php',
-            array('moodle/category:manage', 'moodle/course:create')));
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file defines settingpages and externalpages under the "courses" category
+ *
+ * @package core
+ * @copyright 2002 onwards Martin Dougiamas (http://dougiamas.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$capabilities = array(
+    'moodle/backup:backupcourse',
+    'moodle/category:manage',
+    'moodle/course:create',
+    'moodle/site:approvecourse'
+);
+if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
+    // Speedup for non-admins, add all caps used on this page.
+    $ADMIN->add('courses',
+        new admin_externalpage('coursemgmt', new lang_string('coursemgmt', 'admin'),
+            $CFG->wwwroot . '/course/management.php',
+            array('moodle/category:manage', 'moodle/course:create')
+        )
+    );
+    $ADMIN->add('courses',
+        new admin_externalpage('addcategory', new lang_string('addcategory', 'admin'),
+            new moodle_url('/course/editcategory.php', array('parent' => 0)),
+            array('moodle/category:manage')
+        )
+    );
 
     // Course Default Settings Page.
     // NOTE: these settings must be applied after all other settings because they depend on them.
 
-
     // Main course settings.
     $temp = new admin_settingpage('coursesettings', new lang_string('coursesettings'));
     require_once($CFG->dirroot.'/course/lib.php');
@@ -25,7 +56,6 @@ if ($hassiteconfig
     $temp->add(new admin_setting_configselect('moodlecourse/visible', new lang_string('visible'), new lang_string('visible_help'),
         1, $choices));
 
-
     // Course format.
     $temp->add(new admin_setting_heading('courseformathdr', new lang_string('type_format', 'plugin'), ''));
 
@@ -55,7 +85,6 @@ if ($hassiteconfig
     $temp->add(new admin_setting_configselect('moodlecourse/coursedisplay', new lang_string('coursedisplay'),
         new lang_string('coursedisplay_help'), COURSE_DISPLAY_SINGLEPAGE, $choices));
 
-
     // Appearance.
     $temp->add(new admin_setting_heading('appearancehdr', new lang_string('appearance'), ''));
 
@@ -73,7 +102,6 @@ if ($hassiteconfig
     $temp->add(new admin_setting_configselect('moodlecourse/showreports', new lang_string('showreports'), '', 0,
         array(0 => new lang_string('no'), 1 => new lang_string('yes'))));
 
-
     // Files and uploads.
     $temp->add(new admin_setting_heading('filesanduploadshdr', new lang_string('filesanduploads'), ''));
 
@@ -92,13 +120,11 @@ if ($hassiteconfig
     $temp->add(new admin_setting_configselect('moodlecourse/maxbytes', new lang_string('maximumupload'),
         new lang_string('coursehelpmaximumupload'), key($choices), $choices));
 
-
     // Completion tracking.
     $temp->add(new admin_setting_heading('progress', new lang_string('completion','completion'), ''));
     $temp->add(new admin_setting_configselect('moodlecourse/enablecompletion', new lang_string('completion', 'completion'),
         new lang_string('enablecompletion_help', 'completion'), 0, array(0 => new lang_string('no'), 1 => new lang_string('yes'))));
 
-
     // Groups.
     $temp->add(new admin_setting_heading('groups', new lang_string('groups', 'group'), ''));
     $choices = array();
@@ -119,13 +145,13 @@ if ($hassiteconfig
     $temp->add(new admin_setting_users_with_capability('courserequestnotify', new lang_string('courserequestnotify', 'admin'), new lang_string('configcourserequestnotify2', 'admin'), array(), 'moodle/site:approvecourse'));
     $ADMIN->add('courses', $temp);
 
-/// Pending course requests.
+    // Pending course requests.
     if (!empty($CFG->enablecourserequests)) {
         $ADMIN->add('courses', new admin_externalpage('coursespending', new lang_string('pendingrequests'),
                 $CFG->wwwroot . '/course/pending.php', array('moodle/site:approvecourse')));
     }
 
-    // Add a category for backups
+    // Add a category for backups.
     $ADMIN->add('courses', new admin_category('backups', new lang_string('backups','admin')));
 
     // Create a page for general backups configuration and defaults.
@@ -239,10 +265,8 @@ if ($hassiteconfig
     $temp->add(new admin_setting_configcheckbox('backup/backup_auto_histories', new lang_string('generalhistories','backup'), new lang_string('configgeneralhistories','backup'), 0));
     $temp->add(new admin_setting_configcheckbox('backup/backup_auto_questionbank', new lang_string('generalquestionbank','backup'), new lang_string('configgeneralquestionbank','backup'), 1));
 
-
     //$temp->add(new admin_setting_configcheckbox('backup/backup_auto_messages', new lang_string('messages', 'message'), new lang_string('backupmessageshelp','message'), 0));
     //$temp->add(new admin_setting_configcheckbox('backup/backup_auto_blogs', new lang_string('blogs', 'blog'), new lang_string('backupblogshelp','blog'), 0));
 
     $ADMIN->add('backups', $temp);
-
-} // end of speedup
+}
index 4ec803d..364b54e 100644 (file)
@@ -36,7 +36,7 @@ Feature: Page contents assertions
     And I log in as "admin"
     And I follow "Course 1"
     When I click on "Move this to the dock" "button" in the "Administration" "block"
-    Then I should not see "Question bank"
+    Then I should not see "Question bank" in the "region-pre" "region"
     And I click on "//div[@id='dock']/descendant::h2[normalize-space(.)='Administration']" "xpath_element"
 
   @javascript
@@ -46,4 +46,4 @@ Feature: Page contents assertions
       | Course 1 | C1 | 0 |
     And I log in as "admin"
     When I click on "Move this to the dock" "button" in the "Administration" "block"
-    Then I should not see "Turn editing on"
+    Then I should not see "Turn editing on" in the "region-pre" "region"
index c0d3665..45d1ed6 100644 (file)
@@ -37,9 +37,9 @@ Feature: Set up contextual data for tests
     And I follow "Cat 3"
     And I should see "Course 1"
     And I should see "Course 2"
-    And I select "Cat 1 / Cat 2" from "Course categories:"
+    And I follow "Cat 2"
     And I should see "No courses in this category"
-    And I select "Miscellaneous" from "Course categories:"
+    And I follow "Miscellaneous"
     And I should see "Course 3"
 
   @javascript
index 4bf47d0..2b9ed46 100644 (file)
@@ -32,5 +32,5 @@ Feature: Forms manipulation
     Then I should see "Close the quiz"
     And I should see "Group mode"
     And I should see "Grouping"
-    And I should not see "Show more..."
+    And I should not see "Show more..." in the "region-main-box" "region"
     And I should see "Show less..."
index 3357365..c7e622f 100644 (file)
@@ -165,7 +165,7 @@ function tool_dbtransfer_get_drivers() {
 function tool_dbtransfer_create_maintenance_file() {
     global $CFG;
 
-    register_shutdown_function('tool_dbtransfer_maintenance_callback');
+    core_shutdown_manager::register_function('tool_dbtransfer_maintenance_callback');
 
     $options = new stdClass();
     $options->trusted = false;
index 1dcbee8..3d5653f 100644 (file)
@@ -20,7 +20,7 @@ class cssparser {
 
   function cssparser($html = true) {
     // Register "destructor"
-    register_shutdown_function(array(&$this, "finalize"));
+    core_shutdown_manager::register_function(array(&$this, "finalize"));
     $this->html = ($html != false);
     $this->Clear();
   }
index 29c1f1d..1615dac 100644 (file)
@@ -41,6 +41,8 @@ $result = badges_check_backpack_accessibility();
 $outcome = new stdClass();
 $outcome->code = $result;
 $outcome->response = get_string('error:backpacknotavailable', 'badges') . $OUTPUT->help_icon('backpackavailability', 'badges');
+
+echo $OUTPUT->header();
 echo json_encode($outcome);
 
 die();
index 0bd944e..2e8d3ac 100644 (file)
@@ -24,6 +24,8 @@
  * @author     Yuliya Bozhko <yuliya.bozhko@totaralms.com>
  */
 
+define('AJAX_SCRIPT', true);
+
 require_once(dirname(dirname(__FILE__)) . '/config.php');
 
 if (empty($CFG->enablebadges)) {
@@ -43,6 +45,6 @@ if (!is_null($action)) {
     $json = $assertion->get_badge_assertion();
 }
 
-header('Content-type: application/json; charset=utf-8');
 
+echo $OUTPUT->header();
 echo json_encode($json);
index 55bc2ec..bbcc78f 100644 (file)
@@ -34,6 +34,7 @@ require_sesskey();
 require_login();
 $PAGE->set_url('/badges/backpackconnect.php');
 $PAGE->set_context(context_system::instance());
+echo $OUTPUT->header();
 
 // Use PHP input filtering as there is no PARAM type for
 // the type of cleaning that is required (ASCII chars 32-127 only).
diff --git a/course/ajax/management.php b/course/ajax/management.php
new file mode 100644 (file)
index 0000000..92315a8
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Performs course category management ajax actions.
+ *
+ * Please note functions may throw exceptions, please ensure your JS handles them as well as the outcome objects.
+ *
+ * @package    core_course
+ * @copyright  2013 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('AJAX_SCRIPT', true);
+
+require_once('../../config.php');
+require_once($CFG->dirroot.'/lib/coursecatlib.php');
+require_once($CFG->dirroot.'/course/lib.php');
+
+$action = required_param('action', PARAM_ALPHA);
+require_sesskey(); // Gotta have the sesskey.
+require_login(); // Gotta be logged in (of course).
+
+// Prepare an outcome object. We always use this.
+$outcome = new stdClass;
+$outcome->error = false;
+$outcome->outcome = false;
+
+echo $OUTPUT->header();
+
+switch ($action) {
+    case 'movecourseup' :
+        $courseid = required_param('courseid', PARAM_INT);
+        $outcome->outcome = \core_course\management\helper::action_course_change_sortorder_up_one_by_record($courseid);
+        break;
+    case 'movecoursedown' :
+        $courseid = required_param('courseid', PARAM_INT);
+        $outcome->outcome = \core_course\management\helper::action_course_change_sortorder_down_one_by_record($courseid);
+        break;
+    case 'movecourseintocategory':
+        $courseid = required_param('courseid', PARAM_INT);
+        $categoryid = required_param('categoryid', PARAM_INT);
+        $outcome->outcome = \core_course\management\helper::move_courses_into_category($categoryid, $courseid);
+        break;
+    case 'movecourseafter' :
+        $courseid = required_param('courseid', PARAM_INT);
+        $moveaftercourseid = required_param('moveafter', PARAM_INT);
+        $outcome->outcome = \core_course\management\helper::action_course_change_sortorder_after_course(
+            $courseid, $moveaftercourseid);
+        break;
+    case 'hidecourse' :
+        $courseid = required_param('courseid', PARAM_INT);
+        $outcome->outcome = \core_course\management\helper::action_course_hide_by_record($courseid);
+        break;
+    case 'showcourse' :
+        $courseid = required_param('courseid', PARAM_INT);
+        $outcome->outcome = \core_course\management\helper::action_course_show_by_record($courseid);
+        break;
+    case 'movecategoryup' :
+        $categoryid = required_param('categoryid', PARAM_INT);
+        $outcome->outcome = \core_course\management\helper::action_category_change_sortorder_up_one_by_id($categoryid);
+        break;
+    case 'movecategorydown' :
+        $categoryid = required_param('categoryid', PARAM_INT);
+        $outcome->outcome = \core_course\management\helper::action_category_change_sortorder_down_one_by_id($categoryid);
+        break;
+    case 'hidecategory' :
+        $categoryid = required_param('categoryid', PARAM_INT);
+        $outcome->outcome = \core_course\management\helper::action_category_hide_by_id($categoryid);
+        $outcome->categoryvisibility = \core_course\management\helper::get_category_children_visibility($categoryid);
+        $outcome->coursevisibility = \core_course\management\helper::get_category_courses_visibility($categoryid);
+        break;
+    case 'showcategory' :
+        $categoryid = required_param('categoryid', PARAM_INT);
+        $outcome->outcome = \core_course\management\helper::action_category_show_by_id($categoryid);
+        $outcome->categoryvisibility = \core_course\management\helper::get_category_children_visibility($categoryid);
+        $outcome->coursevisibility = \core_course\management\helper::get_category_courses_visibility($categoryid);
+        break;
+    case 'getsubcategorieshtml' :
+        $categoryid = required_param('categoryid', PARAM_INT);
+        /* @var core_course_management_renderer $renderer */
+        $renderer = $PAGE->get_renderer('core_course', 'management');
+        $outcome->html = html_writer::start_tag('ul', array('class' => 'ml'));
+        $coursecat = coursecat::get($categoryid);
+        foreach ($coursecat->get_children() as $subcat) {
+            $outcome->html .= $renderer->category_listitem($subcat, array(), $subcat->get_children_count());
+        }
+        $outcome->html .= html_writer::end_tag('ul');
+        $outcome->outcome = true;
+        break;
+}
+
+echo json_encode($outcome);
+echo $OUTPUT->footer();
+// Thats all folks.
+// Don't ever even consider putting anything after this. It just wouldn't make sense.
+// But you already knew that, you smart developer you.
+exit;
\ No newline at end of file
index df2a352..89a119d 100644 (file)
 /**
  * Displays the top level category or all courses
  *
- * @package    core
- * @subpackage course
+ * @package    core_coure
  * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
 require_once("../config.php");
 
-$categoryid = required_param('id', PARAM_INT); // Category id
+$categoryid = required_param('id', PARAM_INT); // Category id.
 
 debugging('Please use URL /course/index.php?categoryid=XXX instead of /course/category.php?id=XXX', DEBUG_DEVELOPER);
 
diff --git a/course/classes/deletecategory_form.php b/course/classes/deletecategory_form.php
new file mode 100644 (file)
index 0000000..0d171c4
--- /dev/null
@@ -0,0 +1,143 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Delete category form.
+ *
+ * @package core_course
+ * @copyright 2002 onwards Martin Dougiamas (http://dougiamas.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once($CFG->libdir . '/formslib.php');
+require_once($CFG->libdir . '/questionlib.php');
+require_once($CFG->libdir . '/coursecatlib.php');
+
+/**
+ * Delete category moodleform.
+ * @package core_course
+ * @copyright 2002 onwards Martin Dougiamas (http://dougiamas.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_course_deletecategory_form extends moodleform {
+
+    /**
+     * The coursecat object for that category being deleted.
+     * @var coursecat
+     */
+    protected $coursecat;
+
+    /**
+     * Defines the form.
+     */
+    public function definition() {
+        $mform = $this->_form;
+        $this->coursecat = $this->_customdata;
+
+        $categorycontext = context_coursecat::instance($this->coursecat->id);
+        $categoryname = $this->coursecat->get_formatted_name();
+
+        // Check permissions, to see if it OK to give the option to delete
+        // the contents, rather than move elsewhere.
+        $candeletecontent = $this->coursecat->can_delete_full();
+
+        // Get the list of categories we might be able to move to.
+        $displaylist = $this->coursecat->move_content_targets_list();
+
+        // Now build the options.
+        $options = array();
+        if ($displaylist) {
+            $options[0] = get_string('movecontentstoanothercategory');
+        }
+        if ($candeletecontent) {
+            $options[1] = get_string('deleteallcannotundo');
+        }
+        if (empty($options)) {
+            print_error('youcannotdeletecategory', 'error', 'index.php', $categoryname);
+        }
+
+        // Now build the form.
+        $mform->addElement('header', 'general', get_string('categorycurrentcontents', '', $categoryname));
+
+        // Describe the contents of this category.
+        $contents = '';
+        if ($this->coursecat->has_children()) {
+            $contents .= '<li>' . get_string('subcategories') . '</li>';
+        }
+        if ($this->coursecat->has_courses()) {
+            $contents .= '<li>' . get_string('courses') . '</li>';
+        }
+        if (question_context_has_any_questions($categorycontext)) {
+            $contents .= '<li>' . get_string('questionsinthequestionbank') . '</li>';
+        }
+        if (!empty($contents)) {
+            $mform->addElement('static', 'emptymessage', get_string('thiscategorycontains'), html_writer::tag('ul', $contents));
+        } else {
+            $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->coursecat->parent, $displaylist)) {
+                $mform->setDefault('newparent', $this->coursecat->parent);
+            }
+            $mform->disabledIf('newparent', 'fulldelete', 'eq', '1');
+        }
+
+        $mform->addElement('hidden', 'categoryid', $this->coursecat->id);
+        $mform->setType('categoryid', PARAM_ALPHANUM);
+        $mform->addElement('hidden', 'action', 'deletecategory');
+        $mform->setType('action', PARAM_ALPHANUM);
+        $mform->addElement('hidden', 'sure');
+        // This gets set by default to ensure that if the user changes it manually we can detect it.
+        $mform->setDefault('sure', md5(serialize($this->coursecat)));
+        $mform->setType('sure', PARAM_ALPHANUM);
+
+        $this->add_action_buttons(true, get_string('delete'));
+    }
+
+    /**
+     * Perform some extra moodle validation.
+     *
+     * @param array $data
+     * @param array $files
+     * @return array An array of errors.
+     */
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+        if (empty($data['fulldelete']) && empty($data['newparent'])) {
+            // When they have chosen the move option, they must specify a destination.
+            $errors['newparent'] = get_string('required');
+        }
+
+        if ($data['sure'] !== md5(serialize($this->coursecat))) {
+            $errors['categorylabel'] = get_string('categorymodifiedcancel');
+        }
+
+        return $errors;
+    }
+}
\ No newline at end of file
diff --git a/course/classes/editcategory_form.php b/course/classes/editcategory_form.php
new file mode 100644 (file)
index 0000000..a28ea9b
--- /dev/null
@@ -0,0 +1,134 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Edit category form.
+ *
+ * @package core_course
+ * @copyright 2002 onwards Martin Dougiamas (http://dougiamas.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once($CFG->libdir.'/formslib.php');
+require_once($CFG->libdir.'/coursecatlib.php');
+
+/**
+ * Edit category form.
+ *
+ * @package core_course
+ * @copyright 2002 onwards Martin Dougiamas (http://dougiamas.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_course_editcategory_form extends moodleform {
+
+    /**
+     * The form definition.
+     */
+    public function definition() {
+        global $CFG, $DB;
+        $mform = $this->_form;
+        $categoryid = $this->_customdata['categoryid'];
+        $parent = $this->_customdata['parent'];
+
+        // Get list of categories to use as parents, with site as the first one.
+        $options = array();
+        if (has_capability('moodle/category:manage', context_system::instance()) || $parent == 0) {
+            $options[0] = get_string('top');
+        }
+        if ($categoryid) {
+            // Editing an existing category.
+            $options += coursecat::make_categories_list('moodle/category:manage', $categoryid);
+            if (empty($options[$parent])) {
+                // Ensure the the category parent has been included in the options.
+                $options[$parent] = $DB->get_field('course_categories', 'name', array('id'=>$parent));
+            }
+            $strsubmit = get_string('savechanges');
+        } else {
+            // Making a new category.
+            $options += coursecat::make_categories_list('moodle/category:manage');
+            $strsubmit = get_string('createcategory');
+        }
+
+        $mform->addElement('select', 'parent', get_string('parentcategory'), $options);
+
+        $mform->addElement('text', 'name', get_string('categoryname'), array('size' => '30'));
+        $mform->addRule('name', get_string('required'), 'required', null);
+        $mform->setType('name', PARAM_TEXT);
+
+        $mform->addElement('text', 'idnumber', get_string('idnumbercoursecategory'), 'maxlength="100" size="10"');
+        $mform->addHelpButton('idnumber', 'idnumbercoursecategory');
+        $mform->setType('idnumber', PARAM_RAW);
+
+        $mform->addElement('editor', 'description_editor', get_string('description'), null,
+            $this->get_description_editor_options());
+
+        if (!empty($CFG->allowcategorythemes)) {
+            $themes = array(''=>get_string('forceno'));
+            $allthemes = get_list_of_themes();
+            foreach ($allthemes as $key => $theme) {
+                if (empty($theme->hidefromselector)) {
+                    $themes[$key] = get_string('pluginname', 'theme_'.$theme->name);
+                }
+            }
+            $mform->addElement('select', 'theme', get_string('forcetheme'), $themes);
+        }
+
+        $mform->addElement('hidden', 'id', 0);
+        $mform->setType('id', PARAM_INT);
+        $mform->setDefault('id', $categoryid);
+
+        $this->add_action_buttons(true, $strsubmit);
+    }
+
+    /**
+     * Returns the description editor options.
+     * @return array
+     */
+    public function get_description_editor_options() {
+        global $CFG;
+        $context = $this->_customdata['context'];
+        $itemid = $this->_customdata['itemid'];
+        return array(
+            'maxfiles'  => EDITOR_UNLIMITED_FILES,
+            'maxbytes'  => $CFG->maxbytes,
+            'trusttext' => true,
+            'context'   => $context,
+            'subdirs'   => file_area_contains_subdirs($context, 'coursecat', 'description', $itemid),
+        );
+    }
+
+    /**
+     * Validates the data submit for this form.
+     *
+     * @param array $data An array of key,value data pairs.
+     * @param array $files Any files that may have been submit as well.
+     * @return array An array of errors.
+     */
+    public function validation($data, $files) {
+        global $DB;
+        $errors = parent::validation($data, $files);
+        if (!empty($data['idnumber'])) {
+            if ($existing = $DB->get_record('course_categories', array('idnumber' => $data['idnumber']))) {
+                if (!$data['id'] || $existing->id != $data['id']) {
+                    $errors['idnumber']= get_string('categoryidnumbertaken');
+                }
+            }
+        }
+        return $errors;
+    }
+}
\ No newline at end of file
diff --git a/course/classes/management/helper.php b/course/classes/management/helper.php
new file mode 100644 (file)
index 0000000..aa9e0ac
--- /dev/null
@@ -0,0 +1,806 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Course and category management helper class.
+ *
+ * @package    core_course
+ * @copyright  2013 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_course\management;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Course and category management interface helper class.
+ *
+ * This class provides methods useful to the course and category management interfaces.
+ * Many of the methods on this class are static and serve one of two purposes.
+ *  1.  encapsulate functionality in an effort to ensure minimal changes between the different
+ *      methods of interaction. Specifically browser, AJAX and webservice.
+ *  2.  abstract logic for acquiring actions away from output so that renderers may use them without
+ *      having to include any logic or capability checks.
+ *
+ * @package    core_course
+ * @copyright  2013 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class helper {
+    /**
+     * Returns course details in an array ready to be printed.
+     *
+     * @global \moodle_database $DB
+     * @param \course_in_list $course
+     * @return array
+     */
+    public static function get_course_detail_array(\course_in_list $course) {
+        global $DB;
+
+        $canaccess = $course->can_access();
+
+        $format = \course_get_format($course->id);
+        $modinfo = \get_fast_modinfo($course->id);
+        $modules = $modinfo->get_used_module_names();
+        $sections = array();
+        if ($format->uses_sections()) {
+            foreach ($modinfo->get_section_info_all() as $section) {
+                if ($section->uservisible) {
+                    $sections[] = $format->get_section_name($section);
+                }
+            }
+        }
+
+        $category = \coursecat::get($course->category);
+        $categoryurl = new \moodle_url('/course/management.php', array('categoryid' => $course->category));
+        $categoryname = $category->get_formatted_name();
+
+        $details = array(
+            'fullname' => array(
+                'key' => \get_string('fullname'),
+                'value' => $course->get_formatted_fullname()
+            ),
+            'shortname' => array(
+                'key' => \get_string('shortname'),
+                'value' => $course->get_formatted_shortname()
+            ),
+            'idnumber' => array(
+                'key' => \get_string('idnumber'),
+                'value' => s($course->idnumber)
+            ),
+            'category' => array(
+                'key' => \get_string('category'),
+                'value' => \html_writer::link($categoryurl, $categoryname)
+            )
+        );
+        if (has_capability('moodle/site:accessallgroups', $course->get_context())) {
+            $groups = \groups_get_course_data($course->id);
+            $details += array(
+                'groupings' => array(
+                    'key' => \get_string('groupings', 'group'),
+                    'value' => count($groups->groupings)
+                ),
+                'groups' => array(
+                    'key' => \get_string('groups'),
+                    'value' => count($groups->groups)
+                )
+            );
+        }
+        if ($canaccess) {
+            $names = \role_get_names($course->get_context());
+            $sql = 'SELECT ra.roleid, COUNT(ra.id) AS rolecount
+                      FROM {role_assignments} ra
+                     WHERE ra.contextid = :contextid
+                  GROUP BY ra.roleid';
+            $rolecounts = $DB->get_records_sql($sql, array('contextid' => $course->get_context()->id));
+            $roledetails = array();
+            foreach ($rolecounts as $result) {
+                $a = new \stdClass;
+                $a->role = $names[$result->roleid]->localname;
+                $a->count = $result->rolecount;
+                $roledetails[] = \get_string('assignedrolecount', 'moodle', $a);
+            }
+
+            $details['roleassignments'] = array(
+                'key' => \get_string('roleassignments'),
+                'value' => join('<br />', $roledetails)
+            );
+        }
+        if ($course->can_review_enrolments()) {
+            $enrolmentlines = array();
+            $instances = \enrol_get_instances($course->id, true);
+            $plugins = \enrol_get_plugins(true);
+            foreach ($instances as $instance) {
+                if (!isset($plugins[$instance->enrol])) {
+                    // Weird.
+                    continue;
+                }
+                $plugin = $plugins[$instance->enrol];
+                $enrolmentlines[] = $plugin->get_instance_name($instance);
+            }
+            $details['enrolmentmethods'] = array(
+                'key' => \get_string('enrolmentmethods'),
+                'value' => join('<br />', $enrolmentlines)
+            );
+        }
+        if ($canaccess) {
+            $details['format'] = array(
+                'key' => \get_string('format'),
+                'value' => \course_get_format($course)->get_format_name()
+            );
+            $details['sections'] = array(
+                'key' => \get_string('sections'),
+                'value' => join('<br />', $sections)
+            );
+            $details['modulesused'] = array(
+                'key' => \get_string('modulesused'),
+                'value' =>  join('<br />', $modules)
+            );
+        }
+        return $details;
+    }
+
+    /**
+     * Returns an array of actions that can be performed upon a category being shown in a list.
+     *
+     * @param \coursecat $category
+     * @return array
+     */
+    public static function get_category_listitem_actions(\coursecat $category) {
+        $baseurl = new \moodle_url('/course/management.php', array('categoryid' => $category->id, 'sesskey' => \sesskey()));
+        $actions = array();
+        // Edit.
+        if ($category->can_edit()) {
+            $actions['edit'] = array(
+                'url' => new \moodle_url('/course/editcategory.php', array('id' => $category->id)),
+                'icon' => new \pix_icon('t/edit', new \lang_string('edit')),
+                'string' => new \lang_string('edit')
+            );
+        }
+
+        // Show/Hide.
+        if ($category->can_change_visibility()) {
+            // We always show both icons and then just toggle the display of the invalid option with CSS.
+            $actions['hide'] = array(
+                'url' => new \moodle_url($baseurl, array('action' => 'hidecategory')),
+                'icon' => new \pix_icon('t/hide', new \lang_string('hide')),
+                'string' => new \lang_string('hide')
+            );
+            $actions['show'] = array(
+                'url' => new \moodle_url($baseurl, array('action' => 'showcategory')),
+                'icon' => new \pix_icon('t/show', new \lang_string('show')),
+                'string' => new \lang_string('show')
+            );
+        }
+
+        // Move up/down.
+        if ($category->can_change_sortorder()) {
+            $actions['moveup'] = array(
+                'url' => new \moodle_url($baseurl, array('action' => 'movecategoryup')),
+                'icon' => new \pix_icon('t/up', new \lang_string('up')),
+                'string' => new \lang_string('up')
+            );
+            $actions['movedown'] = array(
+                'url' => new \moodle_url($baseurl, array('action' => 'movecategorydown')),
+                'icon' => new \pix_icon('t/down', new \lang_string('down')),
+                'string' => new \lang_string('down')
+            );
+        }
+
+        // Delete.
+        if ($category->can_delete_full()) {
+            $actions['delete'] = array(
+                'url' => new \moodle_url($baseurl, array('action' => 'deletecategory')),
+                'icon' => new \pix_icon('t/delete', new \lang_string('delete')),
+                'string' => new \lang_string('delete')
+            );
+        }
+
+        // Roles.
+        if ($category->can_review_roles()) {
+            $actions['assignroles'] = array(
+                'url' => new \moodle_url('/admin/roles/assign.php', array('contextid' => $category->get_context()->id,
+                    'return' => 'management')),
+                'icon' => new \pix_icon('i/assignroles', new \lang_string('assignroles', 'role')),
+                'string' => new \lang_string('assignroles', 'role')
+            );
+        }
+
+        // Permissions.
+        if ($category->can_review_permissions()) {
+            $actions['permissions'] = array(
+                'url' => new \moodle_url('/admin/roles/permissions.php', array('contextid' => $category->get_context()->id,
+                    'return' => 'management')),
+                'icon' => new \pix_icon('i/permissions', new \lang_string('permissions', 'role')),
+                'string' => new \lang_string('permissions', 'role')
+            );
+        }
+
+        // Cohorts.
+        if ($category->can_review_cohorts()) {
+            $actions['cohorts'] = array(
+                'url' => new \moodle_url('/cohort/index.php', array('contextid' => $category->get_context()->id)),
+                'icon' => new \pix_icon('i/cohort', new \lang_string('cohorts', 'cohort')),
+                'string' => new \lang_string('cohorts', 'cohort')
+            );
+        }
+
+        // Filters.
+        if ($category->can_review_filters()) {
+            $actions['filters'] = array(
+                'url' => new \moodle_url('/filter/manage.php', array('contextid' => $category->get_context()->id,
+                    'return' => 'management')),
+                'icon' => new \pix_icon('i/filter', new \lang_string('filters', 'admin')),
+                'string' => new \lang_string('filters', 'admin')
+            );
+        }
+
+        return $actions;
+    }
+
+    /**
+     * Returns an array of actions for a course listitem.
+     *
+     * @param \coursecat $category
+     * @param \course_in_list $course
+     * @return string
+     */
+    public static function get_course_listitem_actions(\coursecat $category, \course_in_list $course) {
+        $baseurl = new \moodle_url(
+            '/course/management.php',
+            array('courseid' => $course->id, 'categoryid' => $course->category, 'sesskey' => \sesskey())
+        );
+        $actions = array();
+        // Edit.
+        if ($course->can_edit()) {
+            $actions[] = array(
+                'url' => new \moodle_url('/course/edit.php', array('id' => $course->id)),
+                'icon' => new \pix_icon('t/edit', \get_string('edit')),
+                'attributes' => array('class' => 'action-edit')
+            );
+        }
+        // Show/Hide.
+        if ($course->can_change_visibility()) {
+            $actions[] = array(
+                'url' => new \moodle_url($baseurl, array('action' => 'hidecourse')),
+                'icon' => new \pix_icon('t/hide', \get_string('hide')),
+                'attributes' => array('data-action' => 'hide', 'class' => 'action-hide')
+            );
+            $actions[] = array(
+                'url' => new \moodle_url($baseurl, array('action' => 'showcourse')),
+                'icon' => new \pix_icon('t/show', \get_string('show')),
+                'attributes' => array('data-action' => 'show', 'class' => 'action-show')
+            );
+        }
+        // Move up/down.
+        if ($category->can_resort_courses()) {
+            $actions[] = array(
+                'url' => new \moodle_url($baseurl, array('action' => 'movecourseup')),
+                'icon' => new \pix_icon('t/up', \get_string('up')),
+                'attributes' => array('data-action' => 'moveup', 'class' => 'action-moveup')
+            );
+            $actions[] = array(
+                'url' => new \moodle_url($baseurl, array('action' => 'movecoursedown')),
+                'icon' => new \pix_icon('t/down', \get_string('down')),
+                'attributes' => array('data-action' => 'movedown', 'class' => 'action-movedown')
+            );
+        }
+        return $actions;
+    }
+
+    /**
+     * Returns an array of actions that can be performed on the course being displayed.
+     *
+     * @param \course_in_list $course
+     * @return array
+     */
+    public static function get_course_detail_actions(\course_in_list $course) {
+        $params = array('courseid' => $course->id, 'categoryid' => $course->category, 'sesskey' => \sesskey());
+        $baseurl = new \moodle_url('/course/management.php', $params);
+        $actions = array();
+        // View.
+        if ($course->is_uservisible()) {
+            $actions['view'] = array(
+                'url' => new \moodle_url('/course/view.php', array('id' => $course->id)),
+                'string' => \get_string('view')
+            );
+        }
+        // Edit.
+        if ($course->can_edit()) {
+            $actions['edit'] = array(
+                'url' => new \moodle_url('/course/edit.php', array('id' => $course->id)),
+                'string' => \get_string('edit')
+            );
+        }
+        // Permissions.
+        if ($course->can_review_enrolments()) {
+            $actions['enrolledusers'] = array(
+                'url' => new \moodle_url('/enrol/users.php', array('id' => $course->id)),
+                'string' => \get_string('enrolledusers', 'enrol')
+            );
+        }
+        // Delete.
+        if ($course->can_delete()) {
+            $actions['delete'] = array(
+                'url' => new \moodle_url('/course/delete.php', array('id' => $course->id)),
+                'string' => \get_string('delete')
+            );
+        }
+        // Show/Hide.
+        if ($course->can_change_visibility()) {
+            if ($course->visible) {
+                $actions['hide'] = array(
+                    'url' => new \moodle_url($baseurl, array('action' => 'hidecourse')),
+                    'string' => \get_string('hide')
+                );
+            } else {
+                $actions['show'] = array(
+                    'url' => new \moodle_url($baseurl, array('action' => 'showcourse')),
+                    'string' => \get_string('show')
+                );
+            }
+        }
+        // Backup.
+        if ($course->can_backup()) {
+            $actions['backup'] = array(
+                'url' => new \moodle_url('/backup/backup.php', array('id' => $course->id)),
+                'string' => \get_string('backup')
+            );
+        }
+        // Restore.
+        if ($course->can_restore()) {
+            $actions['restore'] = array(
+                'url' => new \moodle_url('/backup/restorefile.php', array('contextid' => $course->get_context()->id)),
+                'string' => \get_string('restore')
+            );
+        }
+        return $actions;
+    }
+
+    /**
+     * Resorts the courses within a category moving the given course up by one.
+     *
+     * @param \course_in_list $course
+     * @param \coursecat $category
+     * @return bool
+     * @throws \moodle_exception
+     */
+    public static function action_course_change_sortorder_up_one(\course_in_list $course, \coursecat $category) {
+        if (!$category->can_resort_courses()) {
+            throw new \moodle_exception('permissiondenied', 'error', '', null, 'coursecat::can_resort');
+        }
+        return \course_change_sortorder_by_one($course, true);
+    }
+
+    /**
+     * Resorts the courses within a category moving the given course down by one.
+     *
+     * @param \course_in_list $course
+     * @param \coursecat $category
+     * @return bool
+     * @throws \moodle_exception
+     */
+    public static function action_course_change_sortorder_down_one(\course_in_list $course, \coursecat $category) {
+        if (!$category->can_resort_courses()) {
+            throw new \moodle_exception('permissiondenied', 'error', '', null, 'coursecat::can_resort');
+        }
+        return \course_change_sortorder_by_one($course, false);
+    }
+
+    /**
+     * Resorts the courses within a category moving the given course up by one.
+     *
+     * @global \moodle_database $DB
+     * @param int|\stdClass $courserecordorid
+     * @return bool
+     */
+    public static function action_course_change_sortorder_up_one_by_record($courserecordorid) {
+        if (is_int($courserecordorid)) {
+            $courserecordorid = get_course($courserecordorid);
+        }
+        $course = new \course_in_list($courserecordorid);
+        $category = \coursecat::get($course->category);
+        return self::action_course_change_sortorder_up_one($course, $category);
+    }
+
+    /**
+     * Resorts the courses within a category moving the given course down by one.
+     *
+     * @global \moodle_database $DB
+     * @param int|\stdClass $courserecordorid
+     * @return bool
+     */
+    public static function action_course_change_sortorder_down_one_by_record($courserecordorid) {
+        if (is_int($courserecordorid)) {
+            $courserecordorid = get_course($courserecordorid);
+        }
+        $course = new \course_in_list($courserecordorid);
+        $category = \coursecat::get($course->category);
+        return self::action_course_change_sortorder_down_one($course, $category);
+    }
+
+    /**
+     * Changes the sort order so that the first course appears after the second course.
+     *
+     * @param int|\stdClass $courserecordorid
+     * @param int $moveaftercourseid
+     * @return bool
+     * @throws \moodle_exception
+     */
+    public static function action_course_change_sortorder_after_course($courserecordorid, $moveaftercourseid) {
+        $course = \get_course($courserecordorid);
+        $category = \coursecat::get($course->category);
+        if (!$category->can_resort_courses()) {
+            $url = '/course/management.php?categoryid='.$course->category;
+            throw new \moodle_exception('nopermissions', 'error', $url, \get_string('resortcourses', 'moodle'));
+        }
+        return \course_change_sortorder_after_course($course, $moveaftercourseid);
+    }
+
+    /**
+     * Makes a course visible given a \course_in_list object.
+     *
+     * @param \course_in_list $course
+     * @return bool
+     * @throws \moodle_exception
+     */
+    public static function action_course_show(\course_in_list $course) {
+        if (!$course->can_change_visibility()) {
+            throw new \moodle_exception('permissiondenied', 'error', '', null, 'course_in_list::can_change_visbility');
+        }
+        return course_change_visibility($course->id, true);
+    }
+
+    /**
+     * Makes a course hidden given a \course_in_list object.
+     *
+     * @param \course_in_list $course
+     * @return bool
+     * @throws \moodle_exception
+     */
+    public static function action_course_hide(\course_in_list $course) {
+        if (!$course->can_change_visibility()) {
+            throw new \moodle_exception('permissiondenied', 'error', '', null, 'course_in_list::can_change_visbility');
+        }
+        return course_change_visibility($course->id, false);
+    }
+
+    /**
+     * Makes a course visible given a course id or a database record.
+     *
+     * @global \moodle_database $DB
+     * @param int|\stdClass $courserecordorid
+     * @return bool
+     */
+    public static function action_course_show_by_record($courserecordorid) {
+        if (is_int($courserecordorid)) {
+            $courserecordorid = get_course($courserecordorid);
+        }
+        $course = new \course_in_list($courserecordorid);
+        return self::action_course_show($course);
+    }
+
+    /**
+     * Makes a course hidden given a course id or a database record.
+     *
+     * @global \moodle_database $DB
+     * @param int|\stdClass $courserecordorid
+     * @return bool
+     */
+    public static function action_course_hide_by_record($courserecordorid) {
+        if (is_int($courserecordorid)) {
+            $courserecordorid = get_course($courserecordorid);
+        }
+        $course = new \course_in_list($courserecordorid);
+        return self::action_course_hide($course);
+    }
+
+    /**
+     * Resort a categories subcategories shifting the given category up one.
+     *
+     * @param \coursecat $category
+     * @return bool
+     * @throws \moodle_exception
+     */
+    public static function action_category_change_sortorder_up_one(\coursecat $category) {
+        if (!$category->can_change_sortorder()) {
+            throw new \moodle_exception('permissiondenied', 'error', '', null, 'coursecat::can_change_sortorder');
+        }
+        return $category->change_sortorder_by_one(true);
+    }
+
+    /**
+     * Resort a categories subcategories shifting the given category down one.
+     *
+     * @param \coursecat $category
+     * @return bool
+     * @throws \moodle_exception
+     */
+    public static function action_category_change_sortorder_down_one(\coursecat $category) {
+        if (!$category->can_change_sortorder()) {
+            throw new \moodle_exception('permissiondenied', 'error', '', null, 'coursecat::can_change_sortorder');
+        }
+        return $category->change_sortorder_by_one(false);
+    }
+
+    /**
+     * Resort a categories subcategories shifting the given category up one.
+     *
+     * @param int $categoryid
+     * @return bool
+     */
+    public static function action_category_change_sortorder_up_one_by_id($categoryid) {
+        $category = \coursecat::get($categoryid);
+        return self::action_category_change_sortorder_up_one($category);
+    }
+
+    /**
+     * Resort a categories subcategories shifting the given category down one.
+     *
+     * @param int $categoryid
+     * @return bool
+     */
+    public static function action_category_change_sortorder_down_one_by_id($categoryid) {
+        $category = \coursecat::get($categoryid);
+        return self::action_category_change_sortorder_down_one($category);
+    }
+
+    /**
+     * Makes a category hidden given a \coursecat record.
+     *
+     * @param \coursecat $category
+     * @return bool
+     * @throws \moodle_exception
+     */
+    public static function action_category_hide(\coursecat $category) {
+        if (!$category->can_change_visibility()) {
+            throw new \moodle_exception('permissiondenied', 'error', '', null, 'coursecat::can_change_visbility');
+        }
+        $category->hide();
+        return true;
+    }
+
+    /**
+     * Makes a category visible given a \coursecat record.
+     *
+     * @param \coursecat $category
+     * @return bool
+     * @throws \moodle_exception
+     */
+    public static function action_category_show(\coursecat $category) {
+        if (!$category->can_change_visibility()) {
+            throw new \moodle_exception('permissiondenied', 'error', '', null, 'coursecat::can_change_visbility');
+        }
+        if ((int)$category->get_parent_coursecat()->visible === 0) {
+            // You cannot mark a category visible if its parent is hidden.
+            return false;
+        }
+        $category->show();
+        return true;
+    }
+
+    /**
+     * Makes a category visible given a \coursecat id or database record.
+     *
+     * @param int|\stdClass $categoryid
+     * @return bool
+     */
+    public static function action_category_show_by_id($categoryid) {
+        return self::action_category_show(\coursecat::get($categoryid));
+    }
+
+    /**
+     * Makes a category hidden given a \coursecat id or database record.
+     *
+     * @param int|\stdClass $categoryid
+     * @return bool
+     */
+    public static function action_category_hide_by_id($categoryid) {
+        return self::action_category_hide(\coursecat::get($categoryid));
+    }
+
+    /**
+     * Resorts the sub categories of the given category.
+     *
+     * @param \coursecat $category
+     * @param string $sort One of idnumber or name.
+     * @param bool $cleanup If true cleanup will be done, if false you will need to do it manually later.
+     * @return bool
+     * @throws \moodle_exception
+     */
+    public static function action_category_resort_subcategories(\coursecat $category, $sort, $cleanup = true) {
+        if (!$category->can_resort_subcategories()) {
+            throw new \moodle_exception('permissiondenied', 'error', '', null, 'coursecat::can_resort');
+        }
+        return $category->resort_subcategories($sort, $cleanup);
+    }
+
+    /**
+     * Resorts the courses within the given category.
+     *
+     * @param \coursecat $category
+     * @param string $sort One of fullname, shortname or idnumber
+     * @return bool
+     * @throws \moodle_exception
+     */
+    public static function action_category_resort_courses(\coursecat $category, $sort) {
+        if (!$category->can_resort_courses()) {
+            throw new \moodle_exception('permissiondenied', 'error', '', null, 'coursecat::can_resort');
+        }
+        return $category->resort_courses($sort);
+    }
+
+    /**
+     * Moves courses out of one category and into a new category.
+     *
+     * @param \coursecat $oldcategory The category we are moving courses out of.
+     * @param \coursecat $newcategory The category we are moving courses into.
+     * @param array $courseids The ID's of the courses we want to move.
+     * @return bool True on success.
+     * @throws \moodle_exception
+     */
+    public static function action_category_move_courses_into(\coursecat $oldcategory, \coursecat $newcategory, array $courseids) {
+        global $DB;
+
+        list($where, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
+        $params['categoryid'] = $oldcategory->id;
+        $sql = "SELECT c.id FROM {course} c WHERE c.id {$where} AND c.category <> :categoryid";
+        if ($DB->record_exists_sql($sql, $params)) {
+            // Likely being tinkered with.
+            throw new \moodle_exception('coursedoesnotbelongtocategory');
+        }
+
+        // All courses are currently within the old category.
+        return self::move_courses_into_category($newcategory, $courseids);
+    }
+
+    /**
+     * Returns the view modes for the management interface.
+     * @return array
+     */
+    public static function get_management_viewmodes() {
+        return array(
+            'combined' => new \lang_string('categoriesandcoures'),
+            'categories' => new \lang_string('categories'),
+            'courses' => new \lang_string('courses')
+        );
+    }
+
+    /**
+     * Search for courses with matching params.
+     *
+     * Please note that only one of search, blocklist, or modulelist can be specified at a time.
+     * Specifying more than one will result in only the first being used.
+     *
+     * @param string $search Words to search for. We search fullname, shortname, idnumber and summary.
+     * @param int $blocklist The ID of a block, courses will only be returned if they use this block.
+     * @param string $modulelist The name of a module (relates to database table name). Only courses containing this module
+     *      will be returned.
+     * @param int $page The page number to display, starting at 0.
+     * @param int $perpage The number of courses to display per page.
+     * @return array
+     */
+    public static function search_courses($search, $blocklist, $modulelist, $page = 0, $perpage = null) {
+        global $CFG;
+
+        if ($perpage === null) {
+            $perpage = $CFG->coursesperpage;
+        }
+
+        $searchcriteria = array();
+        if (!empty($search)) {
+            $searchcriteria = array('search' => $search);
+        } else if (!empty($blocklist)) {
+            $searchcriteria = array('blocklist' => $blocklist);
+        } else if (!empty($modulelist)) {
+            $searchcriteria = array('modulelist' => $modulelist);
+        }
+
+        $courses = \coursecat::get(0)->search_courses($searchcriteria, array(
+            'recursive' => true,
+            'offset' => $page * $perpage,
+            'limit' => $perpage,
+            'sort' => array('fullname' => 1)
+        ));
+        $totalcount = \coursecat::get(0)->search_courses_count($searchcriteria, array('recursive' => true));
+
+        return array($courses, \count($courses), $totalcount);
+    }
+
+    /**
+     * Moves one or more courses out of the category they are currently in and into a new category.
+     *
+     * This function works much the same way as action_category_move_courses_into however it allows courses from multiple
+     * categories to be moved into a single category.
+     *
+     * @param int|\coursecat $categoryorid The category to move them into.
+     * @param array|int $courseids An array of course id's or optionally just a single course id.
+     * @return bool True on success or false on failure.
+     * @throws \moodle_exception
+     */
+    public static function move_courses_into_category($categoryorid, $courseids = array()) {
+        global $DB;
+        if (!is_array($courseids)) {
+            // Just a single course ID.
+            $courseids = array($courseids);
+        }
+        // Bulk move courses from one category to another.
+        if (count($courseids) === 0) {
+            return false;
+        }
+        if ($categoryorid instanceof \coursecat) {
+            $moveto = $categoryorid;
+        } else {
+            $moveto = \coursecat::get($categoryorid);
+        }
+        if (!$moveto->can_move_courses_out_of() || !$moveto->can_move_courses_into()) {
+            throw new \moodle_exception('cannotmovecourses');
+        }
+
+        list($where, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
+        $params['categoryid'] = $moveto->id;
+        $sql = "SELECT c.id, c.category FROM {course} c WHERE c.id {$where} AND c.category <> :categoryid";
+        $courses = $DB->get_records_sql($sql, $params);
+        $checks = array();
+        foreach ($courseids as $id) {
+            if (!isset($courses[$id])) {
+                throw new \moodle_exception('invalidcourseid');
+            }
+            $catid = $courses[$id]->category;
+            if (!isset($checks[$catid])) {
+                $coursecat = \coursecat::get($catid);
+                $checks[$catid] = $coursecat->can_move_courses_out_of() && $coursecat->can_move_courses_into();
+            }
+            if (!$checks[$catid]) {
+                throw new \moodle_exception('cannotmovecourses');
+            }
+        }
+        return \move_courses($courseids, $moveto->id);
+    }
+
+    /**
+     * Returns an array of courseids and visiblity for all courses within the given category.
+     * @param int $categoryid
+     * @return array
+     */
+    public static function get_category_courses_visibility($categoryid) {
+        global $DB;
+        $sql = "SELECT c.id, c.visible as show
+                  FROM {course} c
+                 WHERE c.category = :category";
+        $params = array('category' => (int)$categoryid);
+        return $DB->get_records_sql($sql, $params);
+    }
+
+    /**
+     * Returns an array of all categoryids that have the given category as a parent and their visible value.
+     * @param int $categoryid
+     * @return array
+     */
+    public static function get_category_children_visibility($categoryid) {
+        global $DB;
+        $category = \coursecat::get($categoryid);
+        $select = $DB->sql_like('path', ':path');
+        $path = $category->path . '/%';
+
+        $sql = "SELECT c.id, c.visible as show
+                  FROM {course_categories} c
+                 WHERE ".$select;
+        $params = array('path' => $path);
+        return $DB->get_records_sql($sql, $params);
+    }
+}
diff --git a/course/classes/management_renderer.php b/course/classes/management_renderer.php
new file mode 100644 (file)
index 0000000..27e5aa4
--- /dev/null
@@ -0,0 +1,1125 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains renderers for the course management pages.
+ *
+ * @package core_course
+ * @copyright 2013 Sam Hemelryk
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once($CFG->dirroot.'/course/renderer.php');
+
+/**
+ * Main renderer for the course management pages.
+ *
+ * @package core_course
+ * @copyright 2013 Sam Hemelryk
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_course_management_renderer extends plugin_renderer_base {
+
+    /**
+     * Initialises the JS required to enhance the management interface.
+     *
+     * Thunderbirds are go, this function kicks into gear the JS that makes the
+     * course management pages that much cooler.
+     */
+    public function enhance_management_interface() {
+        $this->page->requires->yui_module('moodle-course-management', 'M.course.management.init');
+        $this->page->requires->strings_for_js(
+            array('show', 'hide', 'expand', 'collapse', 'confirmcoursemove', 'yes', 'no', 'confirm'),
+            'moodle'
+        );
+    }
+
+    /**
+     * Displays a heading for the management pages.
+     *
+     * @param string $heading The heading to display
+     * @param string|null $viewmode The current view mode if there are options.
+     * @param int|null $categoryid The currently selected category if there is one.
+     * @return string
+     */
+    public function management_heading($heading, $viewmode = null, $categoryid = null) {
+        $html = html_writer::start_div('coursecat-management-header clearfix');
+        if (!empty($heading)) {
+            $html .= $this->heading($heading);
+        }
+        if ($viewmode !== null) {
+            if ($viewmode === 'courses') {
+                $categories = coursecat::make_categories_list(array('moodle/category:manage', 'moodle/course:create'));
+                $nothing = false;
+                if ($categoryid === null) {
+                    $nothing = array('' => get_string('selectacategory'));
+                    $categoryid = '';
+                }
+                $select = new single_select($this->page->url, 'categoryid', $categories, $categoryid, $nothing);
+                $html .= $this->render($select);
+            }
+            $html .= $this->view_mode_selector(\core_course\management\helper::get_management_viewmodes(), $viewmode);
+        }
+        $html .= html_writer::end_div();
+        return $html;
+    }
+
+    /**
+     * Prepares the form element for the course category listing bulk actions.
+     *
+     * @return string
+     */
+    public function management_form_start() {
+        $form = array('action' => $this->page->url->out(), 'method' => 'POST', 'id' => 'coursecat-management');
+
+        $html = html_writer::start_tag('form', $form);
+        $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
+        $html .=  html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'action', 'value' => 'bulkaction'));
+        return $html;
+    }
+
+    /**
+     * Closes the course category bulk management form.
+     *
+     * @return string
+     */
+    public function management_form_end() {
+        return html_writer::end_tag('form');
+    }
+
+    /**
+     * Presents a course category listing.
+     *
+     * @param coursecat $category The currently selected category. Also the category to highlight in the listing.
+     * @return string
+     */
+    public function category_listing(coursecat $category = null) {
+
+        if ($category === null) {
+            $selectedparents = array();
+            $selectedcategory = null;
+        } else {
+            $selectedparents = $category->get_parents();
+            $selectedparents[] = $category->id;
+            $selectedcategory = $category->id;
+        }
+        $catatlevel = array_shift($selectedparents);
+
+        $listing = coursecat::get(0)->get_children();
+
+        $attributes = array(
+            'class' => 'ml',
+            'role' => 'tree',
+            'aria-labelledby' => 'category-listing-title'
+        );
+
+        $html  = html_writer::start_div('category-listing');
+        $html .= html_writer::tag('h3', get_string('categories'), array('id' => 'category-listing-title'));
+        $html .= $this->category_listing_actions($category);
+        $html .= html_writer::start_tag('ul', $attributes);
+        foreach ($listing as $listitem) {
+            // Render each category in the listing.
+            $subcategories = array();
+            if ($listitem->id == $catatlevel) {
+                $subcategories = $listitem->get_children();
+            }
+            $html .= $this->category_listitem(
+                $listitem,
+                $subcategories,
+                $listitem->get_children_count(),
+                $selectedcategory,
+                $selectedparents
+            );
+        }
+        $html .= html_writer::end_tag('ul');
+        $html .= $this->category_bulk_actions();
+        $html .= html_writer::end_div();
+        return $html;
+    }
+
+    /**
+     * Renders a category list item.
+     *
+     * This function gets called recursively to render sub categories.
+     *
+     * @param coursecat $category The category to render as listitem.
+     * @param coursecat[] $subcategories The subcategories belonging to the category being rented.
+     * @param int $totalsubcategories The total number of sub categories.
+     * @param int $selectedcategory The currently selected category
+     * @param int[] $selectedcategories The path to the selected category and its ID.
+     * @return string
+     */
+    public function category_listitem(coursecat $category, array $subcategories, $totalsubcategories,
+                                      $selectedcategory = null, $selectedcategories = array()) {
+        $isexpandable = ($totalsubcategories > 0);
+        $isexpanded = (!empty($subcategories));
+        $activecategory = ($selectedcategory === $category->id);
+        $attributes = array(
+            'class' => 'listitem listitem-category',
+            'data-id' => $category->id,
+            'data-expandable' => $isexpandable ? '1' : '0',
+            'data-expanded' => $isexpanded ? '1' : '0',
+            'data-selected' => $activecategory ? '1' : '0',
+            'data-visible' => $category->visible ? '1' : '0',
+            'role' => 'treeitem',
+            'aria-expanded' => $isexpanded ? 'true' : 'false'
+        );
+        $text = $category->get_formatted_name();
+        $courseicon = $this->output->pix_icon('i/course', get_string('courses'));
+        $bcatinput = array('type' => 'checkbox', 'name' => 'bcat[]', 'value' => $category->id, 'class' => 'bulk-action-checkbox');
+
+        if (!$category->can_resort_subcategories() && !$category->has_manage_capability()) {
+            // Very very hardcoded here.
+            $bcatinput['style'] = 'visibility:hidden';
+        }
+
+        $viewcaturl = new moodle_url('/course/management.php', array('categoryid' => $category->id));
+        if ($isexpanded) {
+            $icon = $this->output->pix_icon('t/switch_minus', get_string('collapse'), 'moodle', array('class' => 'tree-icon'));
+            $icon = html_writer::link($viewcaturl, $icon, array('class' => 'float-left', 'data-action' => 'collapse'));
+        } else if ($isexpandable) {
+            $icon = $this->output->pix_icon('t/switch_plus', get_string('expand'), 'moodle', array('class' => 'tree-icon'));
+            $icon = html_writer::link($viewcaturl, $icon, array('class' => 'float-left', 'data-action' => 'expand'));
+        } else {
+            $icon = $this->output->pix_icon('i/navigationitem', '', 'moodle', array('class' => 'tree-icon'));
+            $icon = html_writer::link($viewcaturl, $icon, array('class' => 'float-left'));
+        }
+        $actions = \core_course\management\helper::get_category_listitem_actions($category);
+        $hasactions = !empty($actions) || $category->can_create_course();
+
+        $html = html_writer::start_tag('li', $attributes);
+        $html .= html_writer::start_div('clearfix');
+        $html .= html_writer::start_div('float-left');
+        $html .= html_writer::empty_tag('input', $bcatinput).'&nbsp;';
+        $html .= html_writer::end_div();
+        $html .= $icon;
+        if ($hasactions) {
+            $html .= html_writer::link($viewcaturl, $text, array('class' => 'float-left categoryname'));
+        } else {
+            $html .= html_writer::link($viewcaturl, $text, array('class' => 'float-left categoryname without-actions'));
+        }
+        $html .= html_writer::start_div('float-right');
+        if ($hasactions) {
+            $html .= $this->category_listitem_actions($category, $actions);
+        }
+        $countid = 'course-count-'.$category->id;
+        $html .= html_writer::span(get_string('courses'), 'accesshide', array('id' => $countid));
+        $html .= html_writer::span($category->coursecount.$courseicon, 'course-count dimmed', array('aria-labelledby' => $countid));
+        $html .= html_writer::end_div();
+        $html .= html_writer::end_div();
+        if ($isexpanded) {
+            $html .= html_writer::start_tag('ul', array('class' => 'ml', 'role' => 'group'));
+            $catatlevel = array_shift($selectedcategories);
+            foreach ($subcategories as $listitem) {
+                $childcategories = ($listitem->id == $catatlevel) ? $listitem->get_children() : array();
+                $html .= $this->category_listitem(
+                    $listitem,
+                    $childcategories,
+                    $listitem->get_children_count(),
+                    $selectedcategory,
+                    $selectedcategories
+                );
+            }
+            $html .= html_writer::end_tag('ul');
+        }
+        $html .= html_writer::end_tag('li');
+        return $html;
+    }
+
+    /**
+     * Renderers the actions that are possible for the course category listing.
+     *
+     * These are not the actions associated with an individual category listing.
+     * That happens through category_listitem_actions.
+     *
+     * @param coursecat $category
+     * @return string
+     */
+    public function category_listing_actions(coursecat $category = null) {
+        $actions = array();
+        $createtoplevel = coursecat::can_create_top_level_category();
+        $createsubcategory = $category && $category->can_create_subcategory();
+        if ($category === null) {
+            $category = coursecat::get(0);
+        }
+
+        $hasitems = false;
+        if ($createtoplevel || $createsubcategory) {
+            $hasitems = true;
+            $menu = new action_menu;
+            if ($createtoplevel) {
+                $url = new moodle_url('/course/editcategory.php', array('parent' => 0));
+                $menu->add(new action_menu_link_secondary(
+                    $url,
+                    null,
+                    get_string('toplevelcategory')
+                ));
+            }
+            if ($createsubcategory) {
+                $url = new moodle_url('/course/editcategory.php', array('parent' => $category->id));
+                $attributes = array(
+                    'title' => get_string('createsubcategoryof', 'moodle', $category->get_formatted_name())
+                );
+                $menu->add(new action_menu_link_secondary(
+                    $url,
+                    null,
+                    get_string('subcategory'),
+                    $attributes
+                ));
+            }
+            $menu->actiontext = get_string('createnew');
+            $menu->actionicon = new pix_icon('t/add', ' ', 'moodle', array('class' => 'iconsmall', 'title' => ''));
+            $actions[] = $this->render($menu);
+        }
+        if (coursecat::can_approve_course_requests()) {
+            $actions[] = html_writer::link(new moodle_url('/course/pending.php'), get_string('coursespending'));
+        }
+        if ($category->can_resort_subcategories()) {
+            $hasitems = true;
+            $params = $this->page->url->params();
+            $params['action'] = 'resortcategories';
+            $params['sesskey'] = sesskey();
+            $baseurl = new moodle_url('/course/management.php', $params);
+            $menu = new action_menu(array(
+                new action_menu_link_secondary(
+                    new moodle_url($baseurl, array('resort' => 'name')),
+                    null,
+                    get_string('resortbyname')
+                ),
+                new action_menu_link_secondary(
+                    new moodle_url($baseurl, array('resort' => 'idnumber')),
+                    null,
+                    get_string('resortbyidnumber')
+                )
+            ));
+            if ($category->id === 0) {
+                $menu->actiontext = get_string('resortcategories');
+            } else {
+                $menu->actiontext = get_string('resortsubcategories');
+            }
+            $menu->actionicon = new pix_icon('t/sort', ' ', 'moodle', array('class' => 'iconsmall', 'title' => ''));
+            $actions[] = $this->render($menu);
+        }
+        if (!$hasitems) {
+            return '';
+        }
+        return html_writer::div(join(' | ', $actions), 'listing-actions category-listing-actions');
+    }
+
+    /**
+     * Renderers the actions for individual category list items.
+     *
+     * @param coursecat $category
+     * @return string
+     */
+    public function category_listitem_actions(coursecat $category, array $actions = null) {
+        if ($actions === null) {
+            $actions = \core_course\management\helper::get_category_listitem_actions($category);
+        }
+        $menu = new action_menu();
+        $menu->attributes['class'] .= ' category-item-actions item-actions';
+        $hasitems = false;
+        foreach ($actions as $key => $action) {
+            $hasitems = true;
+            $menu->add(new action_menu_link(
+                $action['url'],
+                $action['icon'],
+                $action['string'],
+                in_array($key, array('show', 'hide', 'moveup', 'movedown')),
+                array('data-action' => $key, 'class' => 'action-'.$key)
+            ));
+        }
+        if (!$hasitems) {
+            return '';
+        }
+        return $this->render($menu);
+    }
+
+    /**
+     * Renders bulk actions for categories.
+     *
+     * @return string
+     */
+    public function category_bulk_actions() {
+        // Resort courses.
+        // Change parent.
+        $strgo = new lang_string('go');
+
+        $html  = html_writer::start_div('category-bulk-actions bulk-actions');
+        if (coursecat::can_resort_any()) {
+            $options = array(
+                'name' => get_string('resortbyname'),
+                'idnumber' => get_string('resortbyidnumber'),
+            );
+            $select = html_writer::select(
+                $options,
+                'resortcategoriesby',
+                '',
+                array('' => 'choosedots'),
+                array('aria-labelledby' => 'resortselectedcategoriesby')
+            );
+            $submit = array('type' => 'submit', 'name' => 'bulkresortcategories', 'value' => $strgo);
+            $html .= $this->detail_pair(
+                html_writer::span(get_string('resortselectedcategoriesby'), '', array('id' => 'resortselectedcategoriesby')),
+                $select . html_writer::empty_tag('input', $submit)
+            );
+        }
+        if (coursecat::can_change_parent_any()) {
+            $options = coursecat::make_categories_list('moodle/category:manage');
+            $select = html_writer::select(
+                $options,
+                'movecategoriesto',
+                '',
+                array('' => 'choosedots'),
+                array('aria-labelledby' => 'moveselectedcategoriesto')
+            );
+            $submit = array('type' => 'submit', 'name' => 'bulkmovecategories', 'value' => $strgo);
+            $html .= $this->detail_pair(
+                html_writer::span(get_string('moveselectedcategoriesto'), '', array('id' => 'moveselectedcategoriesto')),
+                $select . html_writer::empty_tag('input', $submit)
+            );
+        }
+        $html .= html_writer::end_div();
+        return $html;
+    }
+
+    /**
+     * Renders a course listing.
+     *
+     * @param coursecat $category The currently selected category. This is what the listing is focused on.
+     * @param course_in_list $course The currently selected course.
+     * @param int $page The page being displayed.
+     * @param int $perpage The number of courses to display per page.
+     * @return string
+     */
+    public function course_listing(coursecat $category = null, course_in_list $course = null, $page = 0, $perpage = 20) {
+
+        if ($category === null) {
+            $html = html_writer::start_div('select-a-category');
+            $html .= html_writer::tag('h3', get_string('courses'));
+            $html .= $this->output->notification(get_string('selectacategory'), 'notifymessage');
+            $html .= html_writer::end_div();
+            return $html;
+        }
+
+        $page = max($page, 0);
+        $perpage = max($perpage, 2);
+        $totalcourses = $category->coursecount;
+        $totalpages = ceil($totalcourses / $perpage);
+        if ($page > $totalpages - 1) {
+            $page = $totalpages - 1;
+        }
+        $options = array(
+            'offset' => $page * $perpage,
+            'limit' => $perpage
+        );
+        $courseid = isset($course) ? $course->id : null;
+        $class = '';
+        if ($page === 0) {
+            $class .= ' firstpage';
+        }
+        if ($page + 1 === (int)$totalpages) {
+            $class .= ' lastpage';
+        }
+
+        $html  = html_writer::start_div('course-listing'.$class, array(
+            'data-category' => $category->id,
+            'data-page' => $page,
+            'data-totalpages' => $totalpages,
+            'data-totalcourses' => $totalcourses,
+            'data-canmoveoutof' => $category->can_move_courses_out_of() && $category->can_move_courses_into()
+        ));
+        $html .= html_writer::tag('h3', $category->get_formatted_name());
+        $html .= $this->course_listing_actions($category, $course, $perpage);
+        $html .= $this->listing_pagination($category, $page, $perpage);
+        $html .= html_writer::start_tag('ul', array('class' => 'ml'));
+        foreach ($category->get_courses($options) as $listitem) {
+            $html .= $this->course_listitem($category, $listitem, $courseid);
+        }
+        $html .= html_writer::end_tag('ul');
+        $html .= $this->listing_pagination($category, $page, $perpage, true);
+        $html .= $this->course_bulk_actions($category);
+        $html .= html_writer::end_div();
+        return $html;
+    }
+
+    /**
+     * Renders pagination for a course listing.
+     *
+     * @param coursecat $category The category to produce pagination for.
+     * @param int $page The current page.
+     * @param int $perpage The number of courses to display per page.
+     * @param bool $showtotals Set to true to show the total number of courses and what is being displayed.
+     * @return string
+     */
+    protected function listing_pagination(coursecat $category, $page, $perpage, $showtotals = false) {
+        $html = '';
+        $totalcourses = $category->coursecount;
+        $totalpages = ceil($totalcourses / $perpage);
+        if ($showtotals) {
+            if ($totalpages == 0) {
+                $str = get_string('nocoursesyet');
+            } else if ($totalpages == 1) {
+                $str = get_string('showingacourses', 'moodle', $totalcourses);
+            } else {
+                $a = new stdClass;
+                $a->start = ($page * $perpage) + 1;
+                $a->end = min((($page + 1) * $perpage), $totalcourses);
+                $a->total = $totalcourses;
+                $str = get_string('showingxofycourses', 'moodle', $a);
+            }
+            $html .= html_writer::div($str, 'listing-pagination-totals dimmed');
+        }
+
+        if ($totalcourses < $perpage) {
+            return $html;
+        }
+        $aside = 2;
+        $span = $aside * 2 + 1;
+        $start = max($page - $aside, 0);
+        $end = min($page + $aside, $totalpages - 1);
+        if (($end - $start) < $span) {
+            if ($start == 0) {
+                $end = min($totalpages - 1, $span - 1);
+            } else if ($end == ($totalpages - 1)) {
+                $start = max(0, $end - $span + 1);
+            }
+        }
+        $items = array();
+        $baseurl = new moodle_url('/course/management.php', array('categoryid' => $category->id));
+        if ($page > 0) {
+            $items[] = $this->action_button(new moodle_url($baseurl, array('page' => 0)), get_string('first'));
+            $items[] = $this->action_button(new moodle_url($baseurl, array('page' => $page - 1)), get_string('prev'));
+            $items[] = '...';
+        }
+        for ($i = $start; $i <= $end; $i++) {
+            $class = '';
+            if ($page == $i) {
+                $class = 'active-page';
+            }
+            $pageurl = new moodle_url($baseurl, array('page' => $i));
+            $items[] = $this->action_button($pageurl, $i + 1, null, $class, get_string('pagea', 'moodle', $i+1));
+        }
+        if ($page < ($totalpages - 1)) {
+            $items[] = '...';
+            $items[] = $this->action_button(new moodle_url($baseurl, array('page' => $page + 1)), get_string('next'));
+            $items[] = $this->action_button(new moodle_url($baseurl, array('page' => $totalpages - 1)), get_string('last'));
+        }
+
+        $html .= html_writer::div(join('', $items), 'listing-pagination');
+        return $html;
+    }
+
+    /**
+     * Renderers a course list item.
+     *
+     * This function will be called for every course being displayed by course_listing.
+     *
+     * @param coursecat $category The currently selected category and the category the course belongs to.
+     * @param course_in_list $course The course to produce HTML for.
+     * @param int $selectedcourse The id of the currently selected course.
+     * @return string
+     */
+    public function course_listitem(coursecat $category, course_in_list $course, $selectedcourse) {
+
+        $text = $course->get_formatted_name();
+        $attributes = array(
+            'class' => 'listitem listitem-course',
+            'data-id' => $course->id,
+            'data-selected' => ($selectedcourse == $course->id) ? '1' : '0',
+            'data-visible' => $course->visible ? '1' : '0'
+        );
+
+        $bulkcourseinput = array('type' => 'checkbox', 'name' => 'bc[]', 'value' => $course->id, 'class' => 'bulk-action-checkbox');
+        if (!$category->has_manage_capability()) {
+            // Very very hardcoded here.
+            $bulkcourseinput['style'] = 'visibility:hidden';
+        }
+
+        $viewcourseurl = new moodle_url($this->page->url, array('courseid' => $course->id));
+
+        $html  = html_writer::start_tag('li', $attributes);
+        $html .= html_writer::start_div('clearfix');
+
+        if ($category->can_resort_courses()) {
+            // In order for dnd to be available the user must be able to resort the category children..
+            $html .= html_writer::div($this->output->pix_icon('i/dragdrop', get_string('dndcourse')), 'float-left drag-handle');
+        }
+
+        $html .= html_writer::start_div('float-left');
+        $html .= html_writer::empty_tag('input', $bulkcourseinput).'&nbsp;';
+        $html .= html_writer::end_div();
+        $html .= html_writer::link($viewcourseurl, $text, array('class' => 'float-left coursename'));
+        $html .= html_writer::start_div('float-right');
+        $html .= html_writer::tag('span', s($course->idnumber), array('class' => 'dimmed idnumber'));
+        $html .= $this->course_listitem_actions($category, $course);
+        $html .= html_writer::end_div();
+        $html .= html_writer::end_div();
+        $html .= html_writer::end_tag('li');
+        return $html;
+    }
+
+    /**
+     * Renderers actions for the course listing.
+     *
+     * Not to be confused with course_listitem_actions which renderers the actions for individual courses.
+     *
+     * @param coursecat $category
+     * @param course_in_list $course The currently selected course.
+     * @param int $perpage
+     * @return string
+     */
+    public function course_listing_actions(coursecat $category, course_in_list $course = null, $perpage = 20) {
+        $actions = array();
+        if ($category->can_create_course()) {
+            $url = new moodle_url('/course/edit.php', array('category' => $category->id, 'returnto' => 'catmanage'));
+            $actions[] = html_writer::link($url, get_string('newcourse'));
+        }
+        if ($category->can_request_course()) {
+            // Request a new course.
+            $url = new moodle_url('/course/request.php', array('return' => 'management'));
+            $actions[] = html_writer::link($url, get_string('requestcourse'));
+        }
+        if ($category->can_resort_courses()) {
+            $params = $this->page->url->params();
+            $params['action'] = 'resortcourses';
+            $params['sesskey'] = sesskey();
+            $baseurl = new moodle_url('/course/management.php', $params);
+            $fullnameurl = new moodle_url($baseurl, array('resort' => 'fullname'));
+            $shortnameurl = new moodle_url($baseurl, array('resort' => 'shortname'));
+            $idnumberurl = new moodle_url($baseurl, array('resort' => 'idnumber'));
+            $menu = new action_menu(array(
+                new action_menu_link_secondary($fullnameurl, null, get_string('resortbyfullname')),
+                new action_menu_link_secondary($shortnameurl, null, get_string('resortbyshortname')),
+                new action_menu_link_secondary($idnumberurl, null, get_string('resortbyidnumber'))
+            ));
+            $menu->actiontext = get_string('resortcourses');
+            $menu->actionicon = new pix_icon('t/sort', ' ', 'moodle', array('class' => 'iconsmall', 'title' => ''));
+            $actions[] = $this->render($menu);
+        }
+        $strall = get_string('all');
+        $menu = new action_menu(array(
+            new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 5)), null, 5),
+            new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 10)), null, 10),
+            new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 20)), null, 20),
+            new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 50)), null, 50),
+            new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 100)), null, 100),
+            new action_menu_link_secondary(new moodle_url($this->page->url, array('perpage' => 999)), null, $strall),
+        ));
+        if ((int)$perpage === 999) {
+            $perpage = $strall;
+        }
+        $menu->attributes['class'] .= ' courses-per-page';
+        $menu->actiontext = get_string('perpagea', 'moodle', $perpage);
+        $actions[] = $this->render($menu);
+        return html_writer::div(join(' | ', $actions), 'listing-actions course-listing-actions');
+    }
+
+    /**
+     * Renderers actions for individual course actions.
+     *
+     * @param coursecat $category The currently selected category.
+     * @param course_in_list $course The course to renderer actions for.
+     * @return string
+     */
+    public function course_listitem_actions(coursecat $category, course_in_list $course) {
+        $actions = \core_course\management\helper::get_course_listitem_actions($category, $course);
+        if (empty($actions)) {
+            return '';
+        }
+        $actionshtml = array();
+        foreach ($actions as $action) {
+            $action['attributes']['role'] = 'button';
+            $actionshtml[] = $this->output->action_icon($action['url'], $action['icon'], null, $action['attributes']);
+        }
+        return html_writer::span(join('', $actionshtml), 'course-item-actions item-actions');
+    }
+
+    /**
+     * Renderers bulk actions that can be performed on courses.
+     *
+     * @param coursecat $category The currently selected category and the category in which courses that
+     *      are selectable belong.
+     * @return string
+     */
+    public function course_bulk_actions(coursecat $category) {
+        $html  = html_writer::start_div('course-bulk-actions bulk-actions');
+        if ($category->can_move_courses_out_of()) {
+            $options = coursecat::make_categories_list('moodle/category:manage');
+            $select = html_writer::select(
+                $options,
+                'movecoursesto',
+                '',
+                array('' => 'choosedots'),
+                array('aria-labelledby' => 'moveselectedcoursesto')
+            );
+            $submit = array('type' => 'submit', 'name' => 'bulkmovecourses', 'value' => get_string('go'));
+            $html .= $this->detail_pair(
+                html_writer::span(get_string('moveselectedcoursesto'), '', array('id' => 'moveselectedcoursesto')),
+                $select . html_writer::empty_tag('input', $submit)
+            );
+        }
+        $html .= html_writer::end_div();
+        return $html;
+    }
+
+    /**
+     * Renderers detailed course information.
+     *
+     * @param course_in_list $course The course to display details for.
+     * @return string
+     */
+    public function course_detail(course_in_list $course) {
+        $details = \core_course\management\helper::get_course_detail_array($course);
+        $fullname = $details['fullname']['value'];
+
+        $html  = html_writer::start_div('course-detail');
+        $html .= html_writer::tag('h3', $fullname);
+        $html .= $this->course_detail_actions($course);
+        foreach ($details as $class => $data) {
+            $html .= $this->detail_pair($data['key'], $data['value'], $class);
+        }
+        $html .= html_writer::end_div();
+        return $html;
+    }
+
+    /**
+     * Renderers a key value pair of information for display.
+     *
+     * @param string $key
+     * @param string $value
+     * @param string $class
+     * @return string
+     */
+    protected function detail_pair($key, $value, $class ='') {
+        $html = html_writer::start_div('detail-pair row yui3-g '.preg_replace('#[^a-zA-Z0-9_\-]#', '-', $class));
+        $html .= html_writer::div(html_writer::span($key), 'pair-key span3 yui3-u-1-4');
+        $html .= html_writer::div(html_writer::span($value), 'pair-value span9 yui3-u-3-4');
+        $html .= html_writer::end_div();
+        return $html;
+    }
+
+    /**
+     * A collection of actions for a course.
+     *
+     * @param course_in_list $course The course to display actions for.
+     * @return string
+     */
+    public function course_detail_actions(course_in_list $course) {
+        $actions = \core_course\management\helper::get_course_detail_actions($course);
+        if (empty($actions)) {
+            return '';
+        }
+        $options = array();
+        foreach ($actions as $action) {
+            $options[] = $this->action_link($action['url'], $action['string']);
+        }
+        return html_writer::div(join(' | ', $options), 'listing-actions course-detail-listing-actions');
+    }
+
+    /**
+     * Creates an action button (styled link)
+     *
+     * @param moodle_url $url The URL to go to when clicked.
+     * @param string $text The text for the button.
+     * @param string $id An id to give the button.
+     * @param string $class A class to give the button.
+     * @return string
+     */
+    protected function action_button(moodle_url $url, $text, $id = null, $class = null, $title = null) {
+        $attributes = array(
+            'class' => 'yui3-button',
+        );
+        if (!is_null($id)) {
+            $attributes['id'] = $id;
+        }
+        if (!is_null($class)) {
+            $attributes['class'] .= ' '.$class;
+        }
+        if (is_null($title)) {
+            $title = $text;
+        }
+        $attributes['title'] = $title;
+        return html_writer::link($url, $text, $attributes);
+    }
+
+    /**
+     * Opens a grid.
+     *
+     * Call {@link core_course_management_renderer::grid_column_start()} to create columns.
+     *
+     * @param string $id An id to give this grid.
+     * @param string $class A class to give this grid.
+     * @return string
+     */
+    public function grid_start($id = null, $class = null) {
+        $gridclass = 'grid-row-r row-fluid';
+        if (is_null($class)) {
+            $class = $gridclass;
+        } else {
+            $class .= ' ' . $gridclass;
+        }
+        $attributes = array();
+        if (!is_null($id)) {
+            $attributes['id'] = $id;
+        }
+        return html_writer::start_div($class, $attributes);
+    }
+
+    /**
+     * Closes the grid.
+     *
+     * @return string
+     */
+    public function grid_end() {
+        return html_writer::end_div();
+    }
+
+    /**
+     * Opens a grid column
+     *
+     * @param int $size The number of segments this column should span.
+     * @param string $id An id to give the column.
+     * @param string $class A class to give the column.
+     * @return string
+     */
+    public function grid_column_start($size, $id = null, $class = null) {
+
+        // Calculate Bootstrap grid sizing.
+        $bootstrapclass = 'span'.$size;
+
+        // Calculate YUI grid sizing.
+        if ($size === 12) {
+            $maxsize = 1;
+            $size = 1;
+        } else {
+            $maxsize = 12;
+            $divisors = array(8, 6, 5, 4, 3, 2);
+            foreach ($divisors as $divisor) {
+                if (($maxsize % $divisor === 0) && ($size % $divisor === 0)) {
+                    $maxsize = $maxsize / $divisor;
+                    $size = $size / $divisor;
+                    break;
+                }
+            }
+        }
+        if ($maxsize > 1) {
+            $yuigridclass =  "grid-col-{$size}-{$maxsize} grid-col";
+        } else {
+            $yuigridclass =  "grid-col-1 grid-col";
+        }
+
+        if (is_null($class)) {
+            $class = $yuigridclass . ' ' . $bootstrapclass;
+        } else {
+            $class .= ' ' . $yuigridclass . ' ' . $bootstrapclass;
+        }
+        $attributes = array();
+        if (!is_null($id)) {
+            $attributes['id'] = $id;
+        }
+        return html_writer::start_div($class, $attributes);
+    }
+
+    /**
+     * Closes a grid column.
+     *
+     * @return string
+     */
+    public function grid_column_end() {
+        return html_writer::end_div();
+    }
+
+    /**
+     * Renders an action_icon.
+     *
+     * This function uses the {@link core_renderer::action_link()} method for the
+     * most part. What it does different is prepare the icon as HTML and use it
+     * as the link text.
+     *
+     * @param string|moodle_url $url A string URL or moodel_url
+     * @param pix_icon $pixicon
+     * @param component_action $action
+     * @param array $attributes associative array of html link attributes + disabled
+     * @param bool $linktext show title next to image in link
+     * @return string HTML fragment
+     */
+    public function action_icon($url, pix_icon $pixicon, component_action $action = null,
+                                array $attributes = null, $linktext = false) {
+        if (!($url instanceof moodle_url)) {
+            $url = new moodle_url($url);
+        }
+        $attributes = (array)$attributes;
+
+        if (empty($attributes['class'])) {
+            // Let devs override the class via $attributes.
+            $attributes['class'] = 'action-icon';
+        }
+
+        $icon = $this->render($pixicon);
+
+        if ($linktext) {
+            $text = $pixicon->attributes['alt'];
+        } else {
+            $text = '';
+        }
+
+        return $this->action_link($url, $icon.$text, $action, $attributes);
+    }
+
+    /**
+     * Displays a view mode selector.
+     *
+     * @param array $modes An array of view modes.
+     * @param string $currentmode The current view mode.
+     * @param moodle_url $url The URL to use when changing actions. Defaults to the page URL.
+     * @param string $param The param name.
+     * @return string
+     */
+    public function view_mode_selector(array $modes, $currentmode, moodle_url $url = null, $param = 'view') {
+        if ($url === null) {
+            $url = $this->page->url;
+        }
+
+        $menu = new action_menu;
+        $menu->attributes['class'] .= ' view-mode-selector vms';
+
+        $selected = null;
+        foreach ($modes as $mode => $modestr) {
+            $attributes = array(
+                'class' => 'vms-mode',
+                'data-mode' => $mode
+            );
+            if ($currentmode === $mode) {
+                $attributes['class'] .= ' currentmode';
+                $selected = $modestr;
+            }
+            if ($selected === null) {
+                $selected = $modestr;
+            }
+            $modeurl = new moodle_url($url, array($param => $mode));
+            if ($mode === 'default') {
+                $modeurl->remove_params($param);
+            }
+            $menu->add(new action_menu_link_secondary($modeurl, null, $modestr, $attributes));
+        }
+
+        $menu->actiontext = get_string('viewing', 'moodle', $selected);
+
+        $html = html_writer::start_div('view-mode-selector vms');
+        $html .= $this->render($menu);
+        $html .= html_writer::end_div();
+
+        return $html;
+    }
+
+    /**
+     * Displays a search result listing.
+     *
+     * @param array $courses The courses to display.
+     * @param int $totalcourses The total number of courses to display.
+     * @param course_in_list $course The currently selected course if there is one.
+     * @param int $page The current page, starting at 0.
+     * @param int $perpage The number of courses to display per page.
+     * @return string
+     */
+    public function search_listing(array $courses, $totalcourses, course_in_list $course = null, $page = 0, $perpage = 20) {
+        $page = max($page, 0);
+        $perpage = max($perpage, 2);
+        $totalpages = ceil($totalcourses / $perpage);
+        if ($page > $totalpages - 1) {
+            $page = $totalpages - 1;
+        }
+        $courseid = isset($course) ? $course->id : null;
+        $first = true;
+        $last = false;
+        $i = $page * $perpage;
+
+        $html  = html_writer::start_div('course-listing', array(
+            'data-category' => 'search',
+            'data-page' => $page,
+            'data-totalpages' => $totalpages,
+            'data-totalcourses' => $totalcourses
+        ));
+        $html .= html_writer::tag('h3', get_string('courses'));
+        $html .= $this->search_pagination($totalcourses, $page, $perpage);
+        $html .= html_writer::start_tag('ul', array('class' => 'ml'));
+        foreach ($courses as $listitem) {
+            $i++;
+            if ($i == $totalcourses) {
+                $last = true;
+            }
+            $html .= $this->search_listitem($listitem, $courseid, $first, $last);
+            $first = false;
+        }
+        $html .= html_writer::end_tag('ul');
+        $html .= $this->search_pagination($totalcourses, $page, $perpage, true);
+        $html .= html_writer::end_div();
+        return $html;
+    }
+
+    /**
+     * Displays pagination for search results.
+     *
+     * @param int $totalcourses The total number of courses to be displayed.
+     * @param int $page The current page.
+     * @param int $perpage The number of courses being displayed.
+     * @param bool $showtotals Whether or not to print total information.
+     * @return string
+     */
+    protected function search_pagination($totalcourses, $page, $perpage, $showtotals = false) {
+        $html = '';
+        $totalpages = ceil($totalcourses / $perpage);
+        if ($showtotals) {
+            if ($totalpages == 1) {
+                $str = get_string('showingacourses', 'moodle', $totalcourses);
+            } else {
+                $a = new stdClass;
+                $a->start = ($page * $perpage) + 1;
+                $a->end = min((($page + 1) * $perpage), $totalcourses);
+                $a->total = $totalcourses;
+                $str = get_string('showingxofycourses', 'moodle', $a);
+            }
+            $html .= html_writer::div($str, 'listing-pagination-totals dimmed');
+        }
+
+        if ($totalcourses < $perpage) {
+            return $html;
+        }
+        $aside = 2;
+        $span = $aside * 2 + 1;
+        $start = max($page - $aside, 0);
+        $end = min($page + $aside, $totalpages - 1);
+        if (($end - $start) < $span) {
+            if ($start == 0) {
+                $end = min($totalpages - 1, $span - 1);
+            } else if ($end == ($totalpages - 1)) {
+                $start = max(0, $end - $span + 1);
+            }
+        }
+        $items = array();
+        $baseurl = $this->page->url;
+        if ($page > 0) {
+            $items[] = $this->action_button(new moodle_url($baseurl, array('page' => 0)), get_string('first'));
+            $items[] = $this->action_button(new moodle_url($baseurl, array('page' => $page - 1)), get_string('prev'));
+            $items[] = '...';
+        }
+        for ($i = $start; $i <= $end; $i++) {
+            $class = '';
+            if ($page == $i) {
+                $class = 'active-page';
+            }
+            $items[] = $this->action_button(new moodle_url($baseurl, array('page' => $i)), $i + 1, null, $class);
+        }
+        if ($page < ($totalpages - 1)) {
+            $items[] = '...';
+            $items[] = $this->action_button(new moodle_url($baseurl, array('page' => $page + 1)), get_string('next'));
+            $items[] = $this->action_button(new moodle_url($baseurl, array('page' => $totalpages - 1)), get_string('last'));
+        }
+
+        $html .= html_writer::div(join('', $items), 'listing-pagination');
+        return $html;
+    }
+
+    /**
+     * Renderers a search result course list item.
+     *
+     * This function will be called for every course being displayed by course_listing.
+     *
+     * @param course_in_list $course The course to produce HTML for.
+     * @param int $selectedcourse The id of the currently selected course.
+     * @return string
+     */
+    public function search_listitem(course_in_list $course, $selectedcourse) {
+
+        $text = $course->get_formatted_name();
+        $attributes = array(
+            'class' => 'listitem listitem-course',
+            'data-id' => $course->id,
+            'data-selected' => ($selectedcourse == $course->id) ? '1' : '0',
+            'data-visible' => $course->visible ? '1' : '0'
+        );
+
+        $bulkcourseinput = array('type' => 'checkbox', 'name' => 'bc[]', 'value' => $course->id, 'class' => 'bulk-action-checkbox');
+        $viewcourseurl = new moodle_url($this->page->url, array('courseid' => $course->id));
+        $categoryname = coursecat::get($course->category)->get_formatted_name();
+
+        $html  = html_writer::start_tag('li', $attributes);
+        $html .= html_writer::start_div('clearfix');
+        $html .= html_writer::start_div('float-left');
+        $html .= html_writer::empty_tag('input', $bulkcourseinput).'&nbsp;';
+        $html .= html_writer::end_div();
+        $html .= html_writer::link($viewcourseurl, $text, array('class' => 'float-left coursename'));
+        $html .= html_writer::tag('span', $categoryname, array('class' => 'float-left categoryname'));
+        $html .= html_writer::start_div('float-right');
+        $html .= $this->search_listitem_actions($course);
+        $html .= html_writer::tag('span', s($course->idnumber), array('class' => 'dimmed idnumber'));
+        $html .= html_writer::end_div();
+        $html .= html_writer::end_div();
+        $html .= html_writer::end_tag('li');
+        return $html;
+    }
+
+    /**
+     * Renderers actions for individual course actions.
+     *
+     * @param course_in_list $course The course to renderer actions for.
+     * @return string
+     */
+    public function search_listitem_actions(course_in_list $course) {
+        $baseurl = new moodle_url(
+            '/course/managementsearch.php',
+            array('courseid' => $course->id, 'categoryid' => $course->category, 'sesskey' => sesskey())
+        );
+        $actions = array();
+        // Edit.
+        if ($course->can_access()) {
+            if ($course->can_edit()) {
+                $actions[] = $this->output->action_icon(
+                    new moodle_url('/course/edit.php', array('id' => $course->id)),
+                    new pix_icon('t/edit', get_string('edit')),
+                    null,
+                    array('class' => 'action-edit')
+                );
+            }
+            // Show/Hide.
+            if ($course->can_change_visibility()) {
+                if ($course->visible) {
+                    $actions[] = $this->output->action_icon(
+                        new moodle_url($baseurl, array('action' => 'hidecourse')),
+                        new pix_icon('t/show', get_string('hide')),
+                        null,
+                        array('data-action' => 'hide', 'class' => 'action-hide')
+                    );
+                } else {
+                    $actions[] = $this->output->action_icon(
+                        new moodle_url($baseurl, array('action' => 'showcourse')),
+                        new pix_icon('t/hide', get_string('show')),
+                        null,
+                        array('data-action' => 'show', 'class' => 'action-show')
+                    );
+                }
+            }
+        }
+        if (empty($actions)) {
+            return '';
+        }
+        return html_writer::span(join('', $actions), 'course-item-actions item-actions');
+    }
+
+}
\ No newline at end of file
index 26c3de2..185a05e 100644 (file)
@@ -1,83 +1,82 @@
 <?php
-      // Admin-only code to delete a course utterly
-
-    require_once(dirname(__FILE__) . '/../config.php');
-    require_once($CFG->dirroot . '/course/lib.php');
-
-    $id     = required_param('id', PARAM_INT);              // course id
-    $delete = optional_param('delete', '', PARAM_ALPHANUM); // delete confirmation hash
-
-    $PAGE->set_url('/course/delete.php', array('id' => $id));
-    $PAGE->set_context(context_system::instance());
-    require_login();
-
-    $site = get_site();
-
-    $strdeletecourse = get_string("deletecourse");
-    $stradministration = get_string("administration");
-    $strcategories = get_string("categories");
-
-    if (! $course = $DB->get_record("course", array("id"=>$id))) {
-        print_error("invalidcourseid");
-    }
-    if ($site->id == $course->id) {
-        // can not delete frontpage!
-        print_error("invalidcourseid");
-    }
-
-    $coursecontext = context_course::instance($course->id);
-
-    if (!can_delete_course($id)) {
-        print_error('cannotdeletecourse');
-    }
-
-    $category = $DB->get_record("course_categories", array("id"=>$course->category));
-    $courseshortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id)));
-    $categoryname = format_string($category->name, true, array('context' => context_coursecat::instance($category->id)));
-
-    $PAGE->navbar->add($stradministration, new moodle_url('/admin/index.php/'));
-    $PAGE->navbar->add($strcategories, new moodle_url('/course/index.php'));
-    $PAGE->navbar->add($categoryname, new moodle_url('/course/index.php', array('categoryid' => $course->category)));
-    if (! $delete) {
-        $strdeletecheck = get_string("deletecheck", "", $courseshortname);
-        $strdeletecoursecheck = get_string("deletecoursecheck");
-
-        $PAGE->navbar->add($strdeletecheck);
-        $PAGE->set_title("$site->shortname: $strdeletecheck");
-        $PAGE->set_heading($site->fullname);
-        echo $OUTPUT->header();
-
-        $message = "$strdeletecoursecheck<br /><br />" . format_string($course->fullname, true, array('context' => $coursecontext)) .  " (" . $courseshortname . ")";
-
-        echo $OUTPUT->confirm($message, "delete.php?id=$course->id&delete=".md5($course->timemodified), "manage.php?categoryid=$course->category");
-
-        echo $OUTPUT->footer();
-        exit;
-    }
-
-    if ($delete != md5($course->timemodified)) {
-        print_error("invalidmd5");
-    }
-
-    if (!confirm_sesskey()) {
-        print_error('confirmsesskeybad', 'error');
-    }
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Admin-only code to delete a course utterly.
+ *
+ * @package core_course
+ * @copyright 2002 onwards Martin Dougiamas (http://dougiamas.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(dirname(__FILE__) . '/../config.php');
+require_once($CFG->dirroot . '/course/lib.php');
+
+$id = required_param('id', PARAM_INT); // Course ID.
+$delete = optional_param('delete', '', PARAM_ALPHANUM); // Confirmation hash.
+
+$PAGE->set_url('/course/delete.php', array('id' => $id));
+$PAGE->set_context(context_system::instance());
+require_login();
+
+$course = $DB->get_record('course', array('id' => $id), '*', MUST_EXIST);
+$coursecontext = context_course::instance($course->id);
+
+if ((int)$SITE->id === (int)$course->id || !can_delete_course($id)) {
+    // Can not delete frontpage or don't have permission to delete the course.
+    print_error('cannotdeletecourse');
+}
+
+$courseshortname = format_string($course->shortname, true, array('context' => $coursecontext));
+$coursefullname = format_string($course->fullname, true, array('context' => $coursecontext));
+$categoryurl = new moodle_url('/course/management.php', array('categoryid' => $course->category));
+
+navigation_node::override_active_url(new moodle_url('/course/management.php'));
+
+// Check if we've got confirmation.
+if ($delete === md5($course->timemodified)) {
+    // We do - time to delete the course.
+    require_sesskey();
+    delete_course($course);
+    // Update course count in categories.
+    fix_course_sortorder();
 
     $strdeletingcourse = get_string("deletingcourse", "", $courseshortname);
 
     $PAGE->navbar->add($strdeletingcourse);
-    $PAGE->set_title("$site->shortname: $strdeletingcourse");
-    $PAGE->set_heading($site->fullname);
+    $PAGE->set_title("$SITE->shortname: $strdeletingcourse");
+    $PAGE->set_heading($SITE->fullname);
+
     echo $OUTPUT->header();
     echo $OUTPUT->heading($strdeletingcourse);
-
-    delete_course($course);
-    fix_course_sortorder(); //update course count in catagories
-
     echo $OUTPUT->heading( get_string("deletedcourse", "", $courseshortname) );
-
-    echo $OUTPUT->continue_button("manage.php?categoryid=$course->category");
-
+    echo $OUTPUT->continue_button($categoryurl);
     echo $OUTPUT->footer();
+}
+
+$strdeletecheck = get_string("deletecheck", "", $courseshortname);
+$strdeletecoursecheck = get_string("deletecoursecheck");
+$message = "{$strdeletecoursecheck}<br /><br />{$coursefullname} ({$courseshortname})";
 
+$continueurl = new moodle_url('/course/delete.php', array('id' => $course->id, 'delete' => md5($course->timemodified)));
 
+$PAGE->navbar->add($strdeletecheck);
+$PAGE->set_title("$SITE->shortname: $strdeletecheck");
+$PAGE->set_heading($SITE->fullname);
+echo $OUTPUT->header();
+echo $OUTPUT->confirm($message, $continueurl, $categoryurl);
+echo $OUTPUT->footer();
+exit;
\ No newline at end of file
index f76655e..fa694f4 100644 (file)
 <?php
-
-if (!defined('MOODLE_INTERNAL')) {
-    die('Direct access to this script is forbidden.');    ///  It must be included from a Moodle page
-}
-
-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() {
-        $mform = & $this->_form;
-        $this->_category = $this->_customdata;
-        $categorycontext = context_coursecat::instance($this->_category->id);
-
-        // 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();
-
-        // Get the list of categories we might be able to move to.
-        $displaylist = $this->_category->move_content_targets_list();
-
-        // Now build the options.
-        $options = array();
-        if ($displaylist) {
-            $options[0] = get_string('movecontentstoanothercategory');
-        }
-        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', '', $this->_category->get_formatted_name()));
-
-        // Describe the contents of this category.
-        $contents = '';
-        if ($this->_category->has_children()) {
-            $contents .= '<li>' . get_string('subcategories') . '</li>';
-        }
-        if ($this->_category->has_courses()) {
-            $contents .= '<li>' . get_string('courses') . '</li>';
-        }
-        if (question_context_has_any_questions($categorycontext)) {
-            $contents .= '<li>' . get_string('questionsinthequestionbank') . '</li>';
-        }
-        if (!empty($contents)) {
-            $mform->addElement('static', 'emptymessage', get_string('thiscategorycontains'), html_writer::tag('ul', $contents));
-        } else {
-            $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($this->_category)));
-
-//--------------------------------------------------------------------------------
-        $this->add_action_buttons(true, get_string('delete'));
-
-        $this->set_data(array('deletecat' => $this->_category->id));
-    }
-
-/// perform some extra moodle validation
-    function validation($data, $files) {
-        $errors = parent::validation($data, $files);
-
-        if (empty($data['fulldelete']) && empty($data['newparent'])) {
-        /// When they have chosen the move option, they must specify a destination.
-            $errors['newparent'] = get_string('required');
-        }
-
-        if ($data['sure'] != md5(serialize($this->_category))) {
-            $errors['categorylabel'] = get_string('categorymodifiedcancel');
-        }
-
-        return $errors;
-    }
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+
+/**
+ * Delete category form.
+ *
+ * This file has been deprecated since 2.6.
+ * The class delete_category_form has been renamed to core_course_deletecategory_form and is now autloaded.
+ * Please update your code to use that new class.
+ *
+ * @deprecated since 2.6
+ * @package core_course
+ * @copyright 2002 onwards Martin Dougiamas (http://dougiamas.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Class delete_category_form
+ *
+ * The class delete_category_form has been renamed to core_course_deletecategory_form and is now autloaded.
+ * Please update your code to use that new class.
+ *
+ * @deprecated since 2.6
+ * @todo remove in 2.7 MDL-41502
+ * @package core_course
+ * @copyright 2002 onwards Martin Dougiamas (http://dougiamas.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class delete_category_form extends core_course_deletecategory_form {
+    // Nothing to do here.
 }
 
index 1021c23..f257f91 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
@@ -18,7 +17,7 @@
 /**
  * Edit course settings
  *
- * @package    moodlecore
+ * @package    core_course
  * @copyright  1999 onwards Martin Dougiamas (http://dougiamas.com)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
@@ -27,9 +26,9 @@ require_once('../config.php');
 require_once('lib.php');
 require_once('edit_form.php');
 
-$id         = optional_param('id', 0, PARAM_INT);       // course id
-$categoryid = optional_param('category', 0, PARAM_INT); // course category - can be changed in edit form
-$returnto = optional_param('returnto', 0, PARAM_ALPHANUM); // generic navigation return page switch
+$id = optional_param('id', 0, PARAM_INT); // Course id.
+$categoryid = optional_param('category', 0, PARAM_INT); // Course category - can be changed in edit form.
+$returnto = optional_param('returnto', 0, PARAM_ALPHANUM); // Generic navigation return page switch.
 
 $PAGE->set_pagelayout('admin');
 $pageparams = array('id'=>$id);
@@ -38,10 +37,11 @@ if (empty($id)) {
 }
 $PAGE->set_url('/course/edit.php', $pageparams);
 
-// basic access control checks
-if ($id) { // editing course
+// Basic access control checks.
+if ($id) {
+    // Editing course.
     if ($id == SITEID){
-        // don't allow editing of  'site course' using this from
+        // Don't allow editing of  'site course' using this from.
         print_error('cannoteditsiteform');
     }
 
@@ -51,7 +51,8 @@ if ($id) { // editing course
     $coursecontext = context_course::instance($course->id);
     require_capability('moodle/course:update', $coursecontext);
 
-} else if ($categoryid) { // creating new course in this category
+} else if ($categoryid) {
+    // Creating new course in this category.
     $course = null;
     require_login();
     $category = $DB->get_record('course_categories', array('id'=>$categoryid), '*', MUST_EXIST);
@@ -64,11 +65,11 @@ if ($id) { // editing course
     print_error('needcoursecategroyid');
 }
 
-// Prepare course and the editor
+// Prepare course and the editor.
 $editoroptions = array('maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes'=>$CFG->maxbytes, 'trusttext'=>false, 'noclean'=>true);
 $overviewfilesoptions = course_overviewfiles_options($course);
 if (!empty($course)) {
-    //add context for editor
+    // Add context for editor.
     $editoroptions['context'] = $coursecontext;
     $editoroptions['subdirs'] = file_area_contains_subdirs($coursecontext, 'course', 'summary', 0);
     $course = file_prepare_standard_editor($course, 'summary', $editoroptions, $coursecontext, 'course', 'summary', 0);
@@ -76,14 +77,14 @@ if (!empty($course)) {
         file_prepare_standard_filemanager($course, 'overviewfiles', $overviewfilesoptions, $coursecontext, 'course', 'overviewfiles', 0);
     }
 
-    // Inject current aliases
+    // Inject current aliases.
     $aliases = $DB->get_records('role_names', array('contextid'=>$coursecontext->id));
     foreach($aliases as $alias) {
         $course->{'role_'.$alias->roleid} = $alias->name;
     }
 
 } else {
-    //editor should respect category context if course context is not set.
+    // Editor should respect category context if course context is not set.
     $editoroptions['context'] = $catcontext;
     $editoroptions['subdirs'] = 0;
     $course = file_prepare_standard_editor($course, 'summary', $editoroptions, null, 'course', 'summary', null);
@@ -92,7 +93,7 @@ if (!empty($course)) {
     }
 }
 
-// first create the form
+// First create the form.
 $editform = new course_edit_form(NULL, array('course'=>$course, 'category'=>$category, 'editoroptions'=>$editoroptions, 'returnto'=>$returnto));
 if ($editform->is_cancelled()) {
         switch ($returnto) {
@@ -100,10 +101,10 @@ if ($editform->is_cancelled()) {
                 $url = new moodle_url($CFG->wwwroot.'/course/index.php', array('categoryid' => $categoryid));
                 break;
             case 'catmanage':
-                $url = new moodle_url($CFG->wwwroot.'/course/manage.php', array('categoryid' => $categoryid));
+                $url = new moodle_url($CFG->wwwroot.'/course/management.php', array('categoryid' => $categoryid));
                 break;
             case 'topcatmanage':
-                $url = new moodle_url($CFG->wwwroot.'/course/manage.php');
+                $url = new moodle_url($CFG->wwwroot.'/course/management.php');
                 break;
             case 'topcat':
                 $url = new moodle_url($CFG->wwwroot.'/course/');
@@ -119,34 +120,32 @@ if ($editform->is_cancelled()) {
         redirect($url);
 
 } else if ($data = $editform->get_data()) {
-    // process data if submitted
-
+    // Process data if submitted.
     if (empty($course->id)) {
-        // In creating the course
+        // In creating the course.
         $course = create_course($data, $editoroptions);
 
-        // Get the context of the newly created course
+        // Get the context of the newly created course.
         $context = context_course::instance($course->id, MUST_EXIST);
 
         if (!empty($CFG->creatornewroleid) and !is_viewing($context, NULL, 'moodle/role:assign') and !is_enrolled($context, NULL, 'moodle/role:assign')) {
-            // deal with course creators - enrol them internally with default role
+            // Deal with course creators - enrol them internally with default role.
             enrol_try_internal_enrol($course->id, $USER->id, $CFG->creatornewroleid);
-
         }
         if (!is_enrolled($context)) {
-            // Redirect to manual enrolment page if possible
+            // Redirect to manual enrolment page if possible.
             $instances = enrol_get_instances($course->id, true);
             foreach($instances as $instance) {
                 if ($plugin = enrol_get_plugin($instance->enrol)) {
                     if ($plugin->get_manual_enrol_link($instance)) {
-                        // we know that the ajax enrol UI will have an option to enrol
+                        // We know that the ajax enrol UI will have an option to enrol.
                         redirect(new moodle_url('/enrol/users.php', array('id'=>$course->id)));
                     }
                 }
             }
         }
     } else {
-        // Save any changes to the files used in the editor
+        // Save any changes to the files used in the editor.
         update_course($data, $editoroptions);
     }
 
@@ -154,8 +153,7 @@ if ($editform->is_cancelled()) {
     redirect(new moodle_url('/course/view.php', array('id' => $course->id)));
 }
 
-
-// Print the form
+// Print the form.
 
 $site = get_site();
 
@@ -185,4 +183,3 @@ echo $OUTPUT->heading($streditcoursesettings);
 $editform->display();
 
 echo $OUTPUT->footer();
-
index 05a5777..f2793a2 100644 (file)
 
 /**
  * Page for creating or editing course category name/parent/description.
+ *
  * When called with an id parameter, edits the category with that id.
  * Otherwise it creates a new category with default parent from the parent
  * parameter, which may be 0.
  *
- * @package    core
- * @subpackage course
+ * @package    core_course
  * @copyright  2007 Nicolas Connault
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
 require_once('../config.php');
 require_once($CFG->dirroot.'/course/lib.php');
-require_once($CFG->dirroot.'/course/editcategory_form.php');
 require_once($CFG->libdir.'/coursecatlib.php');
 
 require_login();
 
 $id = optional_param('id', 0, PARAM_INT);
-$itemid = 0; //initalise itemid, as all files in category description has item id 0
 
+$url = new moodle_url('/course/editcategory.php');
 if ($id) {
-    if (!$category = $DB->get_record('course_categories', array('id' => $id))) {
-        print_error('unknowcategory');
-    }
-    $PAGE->set_url('/course/editcategory.php', array('id' => $id));
-    $categorycontext = context_coursecat::instance($id);
-    $PAGE->set_context($categorycontext);
-    require_capability('moodle/category:manage', $categorycontext);
-    $strtitle = get_string('editcategorysettings');
-    $editorcontext = $categorycontext;
+    $coursecat = coursecat::get($id, MUST_EXIST, true);
+    $category = $coursecat->get_db_record();
+    $context = context_coursecat::instance($id);
+
+    $url->param('id', $id);
+    $strtitle = new lang_string('editcategorysettings');
+    $itemid = 0; // Initialise itemid, as all files in category description has item id 0.
     $title = $strtitle;
-    $fullname = $category->name;
+    $fullname = $coursecat->get_formatted_name();
 } else {
     $parent = required_param('parent', PARAM_INT);
-    $PAGE->set_url('/course/editcategory.php', array('parent' => $parent));
+    $url->param('parent', $parent);
     if ($parent) {
-        if (!$DB->record_exists('course_categories', array('id' => $parent))) {
-            print_error('unknowcategory');
-        }
+        $DB->record_exists('course_categories', array('id' => $parent), '*', MUST_EXIST);
         $context = context_coursecat::instance($parent);
     } else {
         $context = context_system::instance();
     }
-    $PAGE->set_context($context);
+
     $category = new stdClass();
     $category->id = 0;
     $category->parent = $parent;
-    require_capability('moodle/category:manage', $context);
-    $strtitle = get_string("addnewcategory");
-    $editorcontext = $context;
-    $itemid = null; //set this explicitly, so files for parent category should not get loaded in draft area.
+    $strtitle = new lang_string("addnewcategory");
+    $itemid = null; // Set this explicitly, so files for parent category should not get loaded in draft area.
     $title = "$SITE->shortname: ".get_string('addnewcategory');
     $fullname = $SITE->fullname;
 }
 
-$PAGE->set_pagelayout('admin');
+require_capability('moodle/category:manage', $context);
 
-$editoroptions = array(
-    'maxfiles'  => EDITOR_UNLIMITED_FILES,
-    'maxbytes'  => $CFG->maxbytes,
-    'trusttext' => true,
-    'context'   => $editorcontext,
-    'subdirs'   => file_area_contains_subdirs($editorcontext, 'coursecat', 'description', $itemid),
-);
-$category = file_prepare_standard_editor($category, 'description', $editoroptions, $editorcontext, 'coursecat', 'description', $itemid);
+// Page "Add new category" (with "Top" as a parent) does not exist in navigation.
+// We pretend we are on course management page.
+if ($id !== 0) {
+    navigation_node::override_active_url(new moodle_url('/course/management.php'));
+}
 
-$mform = new editcategory_form('editcategory.php', compact('category', 'editoroptions'));
-$mform->set_data($category);
+$PAGE->set_context($context);
+$PAGE->set_url($url);
+$PAGE->set_pagelayout('admin');
+$PAGE->set_title($title);
+$PAGE->set_heading($fullname);
 
+$mform = new core_course_editcategory_form(null, array(
+    'categoryid' => $id,
+    'parent' => $category->parent,
+    'context' => $context,
+    'itemid' => $itemid
+));
+$mform->set_data(file_prepare_standard_editor(
+    $category,
+    'description',
+    $mform->get_description_editor_options(),
+    $context,
+    'coursecat',
+    'description',
+    $itemid
+));
+
+$manageurl = new moodle_url('/course/management.php');
 if ($mform->is_cancelled()) {
     if ($id) {
-        redirect($CFG->wwwroot . '/course/manage.php?categoryid=' . $id);
+        $manageurl->param('categoryid', $id);
     } else if ($parent) {
-        redirect($CFG->wwwroot .'/course/manage.php?categoryid=' . $parent);
-    } else {
-        redirect($CFG->wwwroot .'/course/manage.php');
+        $manageurl->param('categoryid', $parent);
     }
+    redirect($manageurl);
 } else if ($data = $mform->get_data()) {
-    if ($id) {
-        $newcategory = coursecat::get($id);
-        if ($data->parent != $category->parent && !$newcategory->can_change_parent($data->parent)) {
+    if (isset($coursecat)) {
+        if ((int)$data->parent !== (int)$coursecat->parent && !$coursecat->can_change_parent($data->parent)) {
             print_error('cannotmovecategory');
         }
-        $newcategory->update($data, $editoroptions);
+        $coursecat->update($data, $mform->get_description_editor_options());
     } else {
-        $newcategory = coursecat::create($data, $editoroptions);
+        $category = coursecat::create($data, $mform->get_description_editor_options());
     }
-
-    redirect('manage.php?categoryid='.$newcategory->id);
+    $manageurl->param('categoryid', $category->id);
+    redirect($manageurl);
 }
 
-// Page "Add new category" (with "Top" as a parent) does not exist in navigation.
-// We pretend we are on course management page.
-if (empty($id) && empty($parent)) {
-    navigation_node::override_active_url(new moodle_url('/course/manage.php'));
-}
-
-$PAGE->set_title($title);
-$PAGE->set_heading($fullname);
 echo $OUTPUT->header();
 echo $OUTPUT->heading($strtitle);
 $mform->display();
 echo $OUTPUT->footer();
-
index 4e0db04..ebff691 100644 (file)
@@ -1,76 +1,66 @@
 <?php
-if (!defined('MOODLE_INTERNAL')) {
-    die('Direct access to this script is forbidden.');    ///  It must be included from a Moodle page
-}
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
-require_once ($CFG->dirroot.'/course/moodleform_mod.php');
-require_once ($CFG->libdir.'/coursecatlib.php');
-class editcategory_form extends moodleform {
+/**
+ * Edit category form.
+ *
+ * This file and class have been deprecated, the form has been renamed to core_course_editcategory_form and is not autoloaded when
+ * first used. Please update your code to use this new form.
+ *
+ * @deprecated since 2.6
+ * @todo remove in 2.7 MDL-41502
+ *
+ * @package core_course
+ * @copyright 2002 onwards Martin Dougiamas (http://dougiamas.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
 
-    // form definition
-    function definition() {
-        global $CFG, $DB;
-        $mform =& $this->_form;
-        $category = $this->_customdata['category'];
-        $editoroptions = $this->_customdata['editoroptions'];
+defined('MOODLE_INTERNAL') || die;
 
-        // get list of categories to use as parents, with site as the first one
-        $options = array();
-        if (has_capability('moodle/category:manage', context_system::instance()) || $category->parent == 0) {
-            $options[0] = get_string('top');
-        }
-        if ($category->id) {
-            // Editing an existing category.
-            $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
-            $options += coursecat::make_categories_list('moodle/category:manage');
-            $strsubmit = get_string('createcategory');
-        }
+debugging('Please update your code to use core_course_editcategory_form (autloaded). This file will be removed in 2.7');
 
-        $mform->addElement('select', 'parent', get_string('parentcategory'), $options);
-        $mform->addElement('text', 'name', get_string('categoryname'), array('size'=>'30'));
-        $mform->addRule('name', get_string('required'), 'required', null);
-        $mform->setType('name', PARAM_TEXT);
-        $mform->addElement('text', 'idnumber', get_string('idnumbercoursecategory'),'maxlength="100"  size="10"');
-        $mform->addHelpButton('idnumber', 'idnumbercoursecategory');
-        $mform->setType('idnumber', PARAM_RAW);
-        $mform->addElement('editor', 'description_editor', get_string('description'), null, $editoroptions);
-        $mform->setType('description_editor', PARAM_RAW);
-        if (!empty($CFG->allowcategorythemes)) {
-            $themes = array(''=>get_string('forceno'));
-            $allthemes = get_list_of_themes();
-            foreach ($allthemes as $key=>$theme) {
-                if (empty($theme->hidefromselector)) {
-                    $themes[$key] = get_string('pluginname', 'theme_'.$theme->name);
-                }
-            }
-            $mform->addElement('select', 'theme', get_string('forcetheme'), $themes);
-        }
+/**
+ * Class editcategory_form.
+ *
+ * This file and class have been deprecated, the form has been renamed to core_course_editcategory_form and is not autoloaded when
+ * first used. Please update your code to use this new form.
+ *
+ * @deprecated since 2.6
+ * @todo remove in 2.7 MDL-41502
+ * @package core_course
+ * @copyright 2002 onwards Martin Dougiamas (http://dougiamas.com)
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class editcategory_form extends core_course_editcategory_form {
 
-        $mform->addElement('hidden', 'id', 0);
-        $mform->setType('id', PARAM_INT);
-        $mform->setDefault('id', $category->id);
-
-        $this->add_action_buttons(true, $strsubmit);
-    }
-
-    function validation($data, $files) {
-        global $DB;
-        $errors = parent::validation($data, $files);
-        if (!empty($data['idnumber'])) {
-            if ($existing = $DB->get_record('course_categories', array('idnumber' => $data['idnumber']))) {
-                if (!$data['id'] || $existing->id != $data['id']) {
-                    $errors['idnumber']= get_string('idnumbertaken');
-                }
-            }
-        }
-
-        return $errors;
+    /**
+     * Constructs the form.
+     * @param null $action
+     * @param null $customdata
+     * @param string $method
+     * @param string $target
+     * @param null $attributes
+     * @param bool $editable
+     */
+    public function __construct($action = null, $customdata = null, $method = 'post', $target = '', $attributes = null,
+                                $editable = true) {
+        $customdata['categoryid'] = $customdata['category']->id;
+        $customdata['parent'] = $customdata['category']->parent;
+        unset($customdata['category']);
+        parent::moodleform($action, $customdata, $method, $target, $attributes, $editable);
     }
-}
 
+};
\ No newline at end of file
index 84017f7..1980441 100644 (file)
@@ -250,6 +250,24 @@ abstract class format_base {
         return $this->course;
     }
 
+    /**
+     * Returns true if the course has a front page.
+     *
+     * This function is called to determine if the course has a view page, whether or not
+     * it contains a listing of activities. It can be useful to set this to false when the course
+     * format has only one activity and ignores the course page. Or if there are multiple
+     * activities but no page to see the centralised information.
+     *
+     * Initially this was created to know if forms should add a button to return to the course page.
+     * So if 'Return to course' does not make sense in your format your should probably return false.
+     *
+     * @return boolean
+     * @since 2.6
+     */
+    public function has_view_page() {
+        return true;
+    }
+
     /**
      * Returns true if this course format uses sections
      *
index 02795d1..396e4d6 100644 (file)
@@ -154,7 +154,7 @@ class format_singleactivity extends format_base {
             );
         }
         if ($foreditform && !isset($courseformatoptions['activitytype']['label'])) {
-            $availabletypes = get_module_types_names();
+            $availabletypes = $this->get_supported_activities();
             $courseformatoptionsedit = array(
                 'activitytype' => array(
                     'label' => new lang_string('activitytype', 'format_singleactivity'),
@@ -270,7 +270,7 @@ class format_singleactivity extends format_base {
      */
     protected function get_activitytype() {
         $options = $this->get_format_options();
-        $availabletypes = get_module_types_names();
+        $availabletypes = $this->get_supported_activities();
         if (!empty($options['activitytype']) &&
                 array_key_exists($options['activitytype'], $availabletypes)) {
             return $options['activitytype'];
@@ -291,6 +291,23 @@ class format_singleactivity extends format_base {
         return $this->activity;
     }
 
+    /**
+     * Get the activities supported by the format.
+     *
+     * Here we ignore the modules that do not have a page of their own, like the label.
+     *
+     * @return array array($module => $name of the module).
+     */
+    public static function get_supported_activities() {
+        $availabletypes = get_module_types_names();
+        foreach ($availabletypes as $module => $name) {
+            if (plugin_supports('mod', $module, FEATURE_NO_VIEW_LINK, false)) {
+                unset($availabletypes[$module]);
+            }
+        }
+        return $availabletypes;
+    }
+
     /**
      * Checks if the current user can add the activity of the specified type to this course.
      *
@@ -440,4 +457,14 @@ class format_singleactivity extends format_base {
             $activitynode->remove();
         }
     }
+
+    /**
+     * Returns true if the course has a front page.
+     *
+     * @return boolean false
+     */
+    public function has_view_page() {
+        return false;
+    }
+
 }
index 75494fc..4846c58 100644 (file)
@@ -42,11 +42,11 @@ class format_singleactivity_admin_setting_activitytype extends admin_setting_con
      */
     public function load_choices() {
         global $CFG;
-        require_once($CFG->dirroot. '/course/lib.php');
+        require_once($CFG->dirroot. '/course/format/singleactivity/lib.php');
         if (is_array($this->choices)) {
             return true;
         }
-        $this->choices = get_module_types_names();
+        $this->choices = format_singleactivity::get_supported_activities();
         return true;
     }
 }
index 65f7875..f717984 100644 (file)
@@ -1,10 +1 @@
-/* Hide confusing form elements "Display description on course page" and
-"Save and return to course" from module edit form because they
-are not applicable in single activity course format */
-body.format-singleactivity.path-mod.pagelayout-admin form.mform #fitem_id_showdescription,
-body.format-singleactivity.path-mod.pagelayout-admin form.mform .fitem_actionbuttons#fgroup_id_buttonar #id_submitbutton {display:none;}
-
-/* In mod_quiz hide "Back to course" button */
-body.format-singleactivity.path-mod-quiz .quizattempt .continuebutton {display:none;}
-
-body.format-singleactivity .tree_item.orphaned a {color:red;}
+.format-singleactivity .tree_item.orphaned a {color:red;}
index e331e2f..3ad88b5 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
@@ -20,8 +19,7 @@
  *
  * @copyright 1999 Martin Dougiamas  http://dougiamas.com
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- * @package core
- * @subpackage course
+ * @package core_course
  */
 
 defined('MOODLE_INTERNAL') || die;
@@ -30,8 +28,8 @@ require_once($CFG->libdir.'/completionlib.php');
 require_once($CFG->libdir.'/filelib.php');
 require_once($CFG->dirroot.'/course/format/lib.php');
 
-define('COURSE_MAX_LOGS_PER_PAGE', 1000);       // records
-define('COURSE_MAX_RECENT_PERIOD', 172800);     // Two days, in seconds
+define('COURSE_MAX_LOGS_PER_PAGE', 1000);       // Records.
+define('COURSE_MAX_RECENT_PERIOD', 172800);     // Two days, in seconds.
 
 /**
  * Number of courses to display when summaries are included.
@@ -40,17 +38,20 @@ define('COURSE_MAX_RECENT_PERIOD', 172800);     // Two days, in seconds
  */
 define('COURSE_MAX_SUMMARIES_PER_PAGE', 10);
 
-define('COURSE_MAX_COURSES_PER_DROPDOWN',1000); //  max courses in log dropdown before switching to optional
-define('COURSE_MAX_USERS_PER_DROPDOWN',1000);   //  max users in log dropdown before switching to optional
-define('FRONTPAGENEWS',           '0');
-define('FRONTPAGECOURSELIST',     '1');         // Not used. TODO MDL-38832 remove
-define('FRONTPAGECATEGORYNAMES',  '2');
-define('FRONTPAGETOPICONLY',      '3');         // Not used. TODO MDL-38832 remove
-define('FRONTPAGECATEGORYCOMBO',  '4');
+// Max courses in log dropdown before switching to optional.
+define('COURSE_MAX_COURSES_PER_DROPDOWN', 1000);
+// Max users in log dropdown before switching to optional.
+define('COURSE_MAX_USERS_PER_DROPDOWN', 1000);
+define('FRONTPAGENEWS', '0');
+define('FRONTPAGECOURSELIST', '1'); // Not used. TODO MDL-38832 remove.
+define('FRONTPAGECATEGORYNAMES', '2');
+define('FRONTPAGETOPICONLY', '3'); // Not used. TODO MDL-38832 remove.
+define('FRONTPAGECATEGORYCOMBO', '4');
 define('FRONTPAGEENROLLEDCOURSELIST', '5');
-define('FRONTPAGEALLCOURSELIST',  '6');
-define('FRONTPAGECOURSESEARCH',   '7');
-define('FRONTPAGECOURSELIMIT',    200);         // Important! Replaced with $CFG->frontpagecourselimit - maximum number of courses displayed on the frontpage. TODO MDL-38832 remove
+define('FRONTPAGEALLCOURSELIST', '6');
+define('FRONTPAGECOURSESEARCH', '7');
+// Important! Replaced with $CFG->frontpagecourselimit - maximum number of courses displayed on the frontpage.
+define('FRONTPAGECOURSELIMIT',    200); // TODO MDL-38832 remove.
 define('EXCELROWS', 65535);
 define('FIRSTUSEDEXCELROW', 3);
 
@@ -1360,7 +1361,7 @@ function get_module_metadata($course, $modnames, $sectionreturn = null) {
  * that if $categoryid is 0, return the system context.
  *
  * @param integer $categoryid a category id or 0.
- * @return object the corresponding context
+ * @return context the corresponding context
  */
 function get_category_or_system_context($categoryid) {
     if ($categoryid) {
@@ -2179,7 +2180,7 @@ function move_courses($courseids, $categoryid) {
 
     if (empty($courseids)) {
         // Nothing to do.
-        return;
+        return false;
     }
 
     if (!$category = $DB->get_record('course_categories', array('id' => $categoryid))) {
@@ -3363,3 +3364,116 @@ function compare_activities_by_time_asc($a, $b) {
     }
     return ($a->timestamp < $b->timestamp) ? -1 : 1;
 }
+
+/**
+ * Changes the visibility of a course.
+ *
+ * @param int $courseid The course to change.
+ * @param bool $show True to make it visible, false otherwise.
+ * @return bool
+ */
+function course_change_visibility($courseid, $show = true) {
+    $course = new stdClass;
+    $course->id = $courseid;
+    $course->visible = ($show) ? '1' : '0';
+    $course->visibleold = $course->visible;
+    update_course($course);
+    return true;
+}
+
+/**
+ * Changes the course sortorder by one, moving it up or down one in respect to sort order.
+ *
+ * @param stdClass|course_in_list $course
+ * @param bool $up If set to true the course will be moved up one. Otherwise down one.
+ * @return bool
+ */
+function course_change_sortorder_by_one($course, $up) {
+    global $DB;
+    $params = array($course->sortorder, $course->category);
+    if ($up) {
+        $select = 'sortorder < ? AND category = ?';
+        $sort = 'sortorder DESC';
+    } else {
+        $select = 'sortorder > ? AND category = ?';
+        $sort = 'sortorder ASC';
+    }
+    fix_course_sortorder();
+    $swapcourse = $DB->get_records_select('course', $select, $params, $sort, '*', 0, 1);
+    if ($swapcourse) {
+        $swapcourse = reset($swapcourse);
+        $DB->set_field('course', 'sortorder', $swapcourse->sortorder, array('id' => $course->id));
+        $DB->set_field('course', 'sortorder', $course->sortorder, array('id' => $swapcourse->id));
+        // Finally reorder courses.
+        fix_course_sortorder();
+        cache_helper::purge_by_event('changesincourse');
+        return true;
+    }
+    return false;
+}
+
+/**
+ * Changes the sort order of courses in a category so that the first course appears after the second.
+ *
+ * @param int|stdClass $courseorid The course to focus on.
+ * @param int $moveaftercourseid The course to shifter after or 0 if you want it to be the first course in the category.
+ * @return bool
+ */
+function course_change_sortorder_after_course($courseorid, $moveaftercourseid) {
+    global $DB;
+
+    if (!is_object($courseorid)) {
+        $course = get_course($courseorid);
+    } else {
+        $course = $courseorid;
+    }
+
+    if ((int)$moveaftercourseid === 0) {
+        // We've moving the course to the start of the queue.
+        $sql = 'SELECT c.sortorder
+                      FROM {course} c
+                     WHERE c.category = :categoryid
+                  ORDER BY c.sortorder';
+        $params = array(
+            'categoryid' => $course->category
+        );
+        $sortorder = $DB->get_field_sql($sql, $params, IGNORE_MULTIPLE);
+
+        $sql = 'UPDATE {course} c
+                   SET sortorder = sortorder + 1
+                 WHERE c.category = :categoryid
+                   AND c.id <> :id';
+        $params = array(
+            'categoryid' => $course->category,
+            'id' => $course->id,
+        );
+        $DB->execute($sql, $params);
+        $DB->set_field('course', 'sortorder', $sortorder, array('id' => $course->id));
+    } else if ($course->id === $moveaftercourseid) {
+        // They're the same - moronic.
+        debugging("Invalid move after course given.", DEBUG_DEVELOPER);
+        return false;
+    } else {
+        // Moving this course after the given course. It could be before it could be after.
+        $moveaftercourse = get_course($moveaftercourseid);
+        if ($course->category !== $moveaftercourse->category) {
+            debugging("Cannot re-order courses. The given courses do not belong to the same category.", DEBUG_DEVELOPER);
+            return false;
+        }
+        // Increment all courses in the same category that are ordered after the moveafter course.
+        // This makes a space for the course we're moving.
+        $sql = 'UPDATE {course} c
+                       SET sortorder = sortorder + 1
+                     WHERE c.category = :categoryid
+                       AND sortorder > :sortorder';
+        $params = array(
+            'categoryid' => $moveaftercourse->category,
+            'sortorder' => $moveaftercourse->sortorder
+        );
+        $DB->execute($sql, $params);
+        $DB->set_field('course', 'sortorder', $moveaftercourse->sortorder + 1, array('id' => $course->id));
+    }
+    fix_course_sortorder();
+    cache_helper::purge_by_event('changesincourse');
+    return true;
+}
index be8d115..8670abd 100644 (file)
 /**
  * Allows the admin to create, delete and rename course categories rearrange courses
  *
- * @package   core
+ * This script has been deprecated since Moodle 2.6.
+ * Please update your links as
+ *
+ * @deprecated
+ * @todo remove in 2.7 MDL-41502
+ * @package   core_course
  * @copyright 2013 Marina Glancy
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
@@ -26,780 +31,128 @@ require_once("../config.php");
 require_once($CFG->dirroot.'/course/lib.php');
 require_once($CFG->libdir.'/coursecatlib.php');
 
-/**
- * Limit the total number of categories where user has 'moodle/course:manage'
- * permission in order for "Move categories" dropdown to be displayed on this page.
- * If number of categories exceeds this limit, user can always use edit category
- * form to change the parent. Otherwise the page size becomes too big.
- */
-if (!defined('COURSECAT_QUICKMOVE_LIMIT')) {
-    define('COURSECAT_QUICKMOVE_LIMIT', 200);
-}
-
-// Category id.
-$id = optional_param('categoryid', 0, PARAM_INT);
-// Which page to show.
-$page = optional_param('page', 0, PARAM_INT);
-// How many per page.
-$perpage = optional_param('perpage', $CFG->coursesperpage, PARAM_INT);
-
-$search    = optional_param('search', '', PARAM_RAW);  // search words
+$id = optional_param('categoryid', 0, PARAM_INT); // Category id.
+$page = optional_param('page', 0, PARAM_INT); // Which page to show.
+$perpage = optional_param('perpage', $CFG->coursesperpage, PARAM_INT); // How many per page.
+$search = optional_param('search', '', PARAM_RAW);  // Search words.
 $blocklist = optional_param('blocklist', 0, PARAM_INT);
-$modulelist= optional_param('modulelist', '', PARAM_PLUGIN);
-if (!$id && !empty($search)) {
-    $searchcriteria = array('search' => $search);
-} else if (!$id && !empty($blocklist)) {
-    $searchcriteria = array('blocklist' => $blocklist);
-} else if (!$id && !empty($modulelist)) {
-    $searchcriteria = array('modulelist' => $modulelist);
-} else {
-    $searchcriteria = array();
-}
-
-// Actions to manage courses.
-$hide = optional_param('hide', 0, PARAM_INT);
-$show = optional_param('show', 0, PARAM_INT);
-$moveup = optional_param('moveup', 0, PARAM_INT);
-$movedown = optional_param('movedown', 0, PARAM_INT);
-$moveto = optional_param('moveto', 0, PARAM_INT);
-$resort = optional_param('resort', 0, PARAM_BOOL);
-
-// Actions to manage categories.
-$deletecat = optional_param('deletecat', 0, PARAM_INT);
-$hidecat = optional_param('hidecat', 0, PARAM_INT);
-$showcat = optional_param('showcat', 0, PARAM_INT);
-$movecat = optional_param('movecat', 0, PARAM_INT);
-$movetocat = optional_param('movetocat', -1, PARAM_INT);
-$moveupcat = optional_param('moveupcat', 0, PARAM_INT);
-$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('categoryid' => $id)));
-    // This is sure to be the category context.
-    $context = $PAGE->context;
-    if (!can_edit_in_category($coursecat->id)) {
-        redirect(new moodle_url('/course/index.php', array('categoryid' => $coursecat->id)));
-    }
-} else {
-    $context = context_system::instance();
-    $PAGE->set_context($context);
-    $PAGE->set_url(new moodle_url('/course/manage.php'));
-    if (!can_edit_in_category()) {
-        redirect(new moodle_url('/course/index.php'));
-    }
-}
-
-$canmanage = has_capability('moodle/category:manage', $context);
-
-// Process any category actions.
-if (!empty($deletecat) and confirm_sesskey()) {
-    // Delete a category.
-    $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));
-
-    $heading = get_string('deletecategory', 'moodle', format_string($cattodelete->name, true, array('context' => $context)));
-
-    require_once($CFG->dirroot.'/course/delete_category_form.php');
-    $mform = new delete_category_form(null, $cattodelete);
-    if ($mform->is_cancelled()) {
-        redirect(new moodle_url('/course/manage.php'));
-    }
-
-    // Start output.
-    echo $OUTPUT->header();
-    echo $OUTPUT->heading($heading);
-
-    if ($data = $mform->get_data()) {
-        // The form has been submit handle it.
-        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');
-            }
-            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'));
+$modulelist = optional_param('modulelist', '', PARAM_PLUGIN);
+
+debugging('This script has been deprecated and will be removed in the future. Please update any bookmarks you have.',
+    DEBUG_DEVELOPER);
+
+// Look for legacy actions.
+// If there are any we're going to make and equivalent request to management.php.
+$sesskey = optional_param('sesskey', null, PARAM_RAW);
+if ($sesskey !== null && confirm_sesskey($sesskey)) {
+    // Actions to manage categories.
+    $deletecat = optional_param('deletecat', 0, PARAM_INT);
+    $hidecat = optional_param('hidecat', 0, PARAM_INT);
+    $showcat = optional_param('showcat', 0, PARAM_INT);
+    $movecat = optional_param('movecat', 0, PARAM_INT);
+    $movetocat = optional_param('movetocat', -1, PARAM_INT);
+    $moveupcat = optional_param('moveupcat', 0, PARAM_INT);
+    $movedowncat = optional_param('movedowncat', 0, PARAM_INT);
+
+    // Actions to manage courses.
+    $hide = optional_param('hide', 0, PARAM_INT);
+    $show = optional_param('show', 0, PARAM_INT);
+    $moveup = optional_param('moveup', 0, PARAM_INT);
+    $movedown = optional_param('movedown', 0, PARAM_INT);
+    $moveto = optional_param('moveto', 0, PARAM_INT);
+    $resort = optional_param('resort', 0, PARAM_BOOL);
+
+    $murl = new moodle_url('/course/management.php', array('sesskey' => sesskey()));
+
+    // Process any category actions.
+    if (!empty($deletecat)) {
+        // Redirect to the new management script.
+        redirect(new moodle_url($murl, array('categoryid' => $deletecat, 'action' => 'deletecategory')));
+    }
+
+    if (!empty($movecat) and $movetocat >= 0) {
+        // Redirect to the new management script.
+        redirect(new moodle_url($murl, array(
+            'action' => 'bulkaction',
+            'bulkmovecategories' => true,
+            'movecategoriesto' => $movetocat,
+            'bcat[]' => $movecat
+        )));
+    }
+
+    // Hide or show a category.
+    if ($hidecat) {
+        // Redirect to the new management script.
+        redirect(new moodle_url($murl, array('categoryid' => $hidecat, 'action' => 'hidecategory')));
+    } else if ($showcat) {
+        // Redirect to the new management script.
+        redirect(new moodle_url($murl, array('categoryid' => $showcat, 'action' => 'showcategory')));
+    }
+
+    if (!empty($moveupcat) or !empty($movedowncat)) {
+        // Redirect to the new management script.
+        if (!empty($moveupcat)) {
+            redirect(new moodle_url($murl, array('categoryid' => $moveupcat, 'action' => 'movecategoryup')));
         } else {
-            // Some error in parameters (user is cheating?)
-            $mform->display();
+            redirect(new moodle_url($murl, array('categoryid' => $movedowncat, 'action' => 'movecategorydown')));
         }
-    } else {
-        // Display the form.
-        $mform->display();
     }
-    // Finish output and exit.
-    echo $OUTPUT->footer();
-    exit();
-}
 
-if (!empty($movecat) and ($movetocat >= 0) and confirm_sesskey()) {
-    // Move a category to a new parent if required.
-    $cattomove = coursecat::get($movecat);
-    if ($cattomove->parent != $movetocat) {
-        if ($cattomove->can_change_parent($movetocat)) {
-            $cattomove->change_parent($movetocat);
-        } else {
-            print_error('cannotmovecategory');
-        }
+    if ($resort && $id) {
+        // Redirect to the new management script.
+        redirect(new moodle_url($murl, array('categoryid' => $id, 'action' => 'resortcategories', 'resort' => 'name')));
     }
-}
-
-// Hide or show a category.
-if ($hidecat and confirm_sesskey()) {
-    $cattohide = coursecat::get($hidecat);
-    require_capability('moodle/category:manage', get_category_or_system_context($cattohide->parent));
-    $cattohide->hide();
-} else if ($showcat and confirm_sesskey()) {
-    $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()) {
-    // Move a category up or down.
-    fix_course_sortorder();
-    $swapcategory = null;
 
-    if (!empty($moveupcat)) {
-        require_capability('moodle/category:manage', context_coursecat::instance($moveupcat));
-        if ($movecategory = $DB->get_record('course_categories', array('id' => $moveupcat))) {
-            $params = array($movecategory->sortorder, $movecategory->parent);
-            if ($swapcategory = $DB->get_records_select('course_categories', "sortorder<? AND parent=?", $params, 'sortorder DESC', '*', 0, 1)) {
-                $swapcategory = reset($swapcategory);
+    if (!empty($moveto) && ($data = data_submitted())) {
+        // Redirect to the new management script.
+        $courses = array();
+        foreach ($data as $key => $value) {
+            if (preg_match('/^c\d+$/', $key)) {
+                $courses[] = substr($key, 1);
             }
         }
-    } else {
-        require_capability('moodle/category:manage', context_coursecat::instance($movedowncat));
-        if ($movecategory = $DB->get_record('course_categories', array('id' => $movedowncat))) {
-            $params = array($movecategory->sortorder, $movecategory->parent);
-            if ($swapcategory = $DB->get_records_select('course_categories', "sortorder>? AND parent=?", $params, 'sortorder ASC', '*', 0, 1)) {
-                $swapcategory = reset($swapcategory);
-            }
-        }
-    }
-    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));
-        cache_helper::purge_by_event('changesincoursecat');
-        add_to_log(SITEID, "category", "move", "editcategory.php?id=$movecategory->id", $movecategory->id);
-    }
-
-    // Finally reorder courses.
-    fix_course_sortorder();
-}
-
-if ($coursecat->id && $canmanage && $resort && confirm_sesskey()) {
-    // Resort the category.
-    if ($courses = get_courses($coursecat->id, '', 'c.id,c.fullname,c.sortorder')) {
-        core_collator::asort_objects_by_property($courses, 'fullname', core_collator::SORT_NATURAL);
-        $i = 1;
-        foreach ($courses as $course) {
-            $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');
-    }
-}
-
-if (!empty($moveto) && ($data = data_submitted()) && confirm_sesskey()) {
-    // Move a specified course to a new category.
-    // User must have category update in both cats to perform this.
-    require_capability('moodle/category:manage', $context);
-    require_capability('moodle/category:manage', context_coursecat::instance($moveto));
-
-    if (!$destcategory = $DB->get_record('course_categories', array('id' => $data->moveto))) {
-        print_error('cannotfindcategory', '', '', $data->moveto);
-    }
-
-    $courses = array();
-    foreach ($data as $key => $value) {
-        if (preg_match('/^c\d+$/', $key)) {
-            $courseid = substr($key, 1);
-            array_push($courses, $courseid);
-            // Check this course's category.
-            if ($movingcourse = $DB->get_record('course', array('id' => $courseid))) {
-                if ($id && $movingcourse->category != $id ) {
-                    print_error('coursedoesnotbelongtocategory');
-                }
-            } else {
-                print_error('cannotfindcourse');
-            }
-        }
-    }
-    move_courses($courses, $data->moveto);
-}
-
-if ((!empty($hide) or !empty($show)) && confirm_sesskey()) {
-    // Hide or show a course.
-    if (!empty($hide)) {
-        $course = $DB->get_record('course', array('id' => $hide), '*', MUST_EXIST);
-        $visible = 0;
-    } else {
-        $course = $DB->get_record('course', array('id' => $show), '*', MUST_EXIST);
-        $visible = 1;
-    }
-    $coursecontext = context_course::instance($course->id);
-    require_capability('moodle/course:visibility', $coursecontext);
-    // 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');
-
-    // Update the course object we pass to the event class.
-    $course->visible = $params['visible'];
-    $course->visibleold = $params['visibleold'];
-    $course->timemodified = $params['timemodified'];
-
-    // Trigger a course updated event.
-    $event = \core\event\course_updated::create(array(
-        'objectid' => $course->id,
-        'context' => $coursecontext,
-        'other' => array('shortname' => $course->shortname,
-                         'fullname' => $course->fullname)
-    ));
-    $event->add_record_snapshot('course', $course);
-    $event->set_legacy_logdata(array($course->id, 'course', ($visible ? 'show' : 'hide'), 'edit.php?id=' . $course->id, $course->id));
-    $event->trigger();
-}
-
-if ((!empty($moveup) or !empty($movedown)) && confirm_sesskey()) {
-    // Move a course up or down.
-    require_capability('moodle/category:manage', $context);
-
-    // Ensure the course order has continuous ordering.
-    fix_course_sortorder();
-    $swapcourse = null;
-
-    if (!empty($moveup)) {
-        if ($movecourse = $DB->get_record('course', array('id' => $moveup))) {
-            $swapcourse = $DB->get_record('course', array('sortorder' => $movecourse->sortorder - 1));
-        }
-    } else {
-        if ($movecourse = $DB->get_record('course', array('id' => $movedown))) {
-            $swapcourse = $DB->get_record('course', array('sortorder' => $movecourse->sortorder + 1));
-        }
-    }
-    if ($swapcourse and $movecourse) {
-        // Check course's category.
-        if ($movecourse->category != $id) {
-            print_error('coursedoesnotbelongtocategory');
-        }
-        $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');
-
-        // Update $movecourse's sortorder.
-        $movecourse->sortorder = $swapcourse->sortorder;
-
-        // Trigger a course updated event.
-        $event = \core\event\course_updated::create(array(
-            'objectid' => $movecourse->id,
-            'context' => context_course::instance($movecourse->id),
-            'other' => array('shortname' => $movecourse->shortname,
-                             'fullname' => $movecourse->fullname)
-        ));
-        $event->add_record_snapshot('course', $movecourse);
-        $event->set_legacy_logdata(array($movecourse->id, 'course', 'move', 'edit.php?id=' . $movecourse->id, $movecourse->id));
-        $event->trigger();
-    }
-}
-
-// Prepare the standard URL params for this page. We'll need them later.
-$urlparams = array('categoryid' => $id);
-if ($page) {
-    $urlparams['page'] = $page;
-}
-if ($perpage) {
-    $urlparams['perpage'] = $perpage;
-}
-$urlparams += $searchcriteria;
-
-$PAGE->set_pagelayout('coursecategory');
-$courserenderer = $PAGE->get_renderer('core', 'course');
-
-if (can_edit_in_category()) {
-    // Integrate into the admin tree only if the user can edit categories at the top level,
-    // otherwise the admin block does not appear to this user, and you get an error.
-    require_once($CFG->libdir . '/adminlib.php');
-    if ($id) {
-        navigation_node::override_active_url(new moodle_url('/course/index.php', array('categoryid' => $id)));
-    }
-    admin_externalpage_setup('coursemgmt', '', $urlparams, $CFG->wwwroot . '/course/manage.php');
-    $settingsnode = $PAGE->settingsnav->find_active_node();
-    if ($id && $settingsnode) {
-        $settingsnode->make_inactive();
-        $settingsnode->force_open();
-        $PAGE->navbar->add($settingsnode->text, $settingsnode->action);
-    }
-} else {
-    $site = get_site();
-    $PAGE->set_title("$site->shortname: $coursecat->name");
-    $PAGE->set_heading($site->fullname);
-    $PAGE->set_button($courserenderer->course_search_form('', 'navbar'));
-}
-
-// Start output.
-echo $OUTPUT->header();
-
-if (!empty($searchcriteria)) {
-    echo $OUTPUT->heading(new lang_string('searchresults'));
-} else if (!$coursecat->id) {
-    // Print out the categories with all the knobs.
-    $manageablecategories = coursecat::make_categories_list('moodle/category:manage');
-    $displaymovecategoryto = !empty($manageablecategories) && (count($manageablecategories) <= COURSECAT_QUICKMOVE_LIMIT);
-    $table = new html_table;
-    $table->id = 'coursecategories';
-    $table->attributes['class'] = 'admintable generaltable editcourse';
-    $table->head = array(
-        get_string('categories'),
-        get_string('courses'),
-        get_string('edit'),
-    );
-    if ($displaymovecategoryto) {
-        $table->head[] = get_string('movecategoryto');
-    }
-    $table->colclasses = array(
-        'leftalign name',
-        'centeralign count',
-        'centeralign icons',
-    );
-    if ($displaymovecategoryto) {
-        $table->colclasses[] = 'leftalign actions';
-    }
-    $table->data = array();
-
-    print_category_edit($table, $coursecat, -1, false, false, $displaymovecategoryto);
-
-    echo html_writer::table($table);
-} else {
-    // Print the category selector.
-    $displaylist = coursecat::make_categories_list();
-    $select = new single_select(new moodle_url('/course/manage.php'), 'categoryid', $displaylist, $coursecat->id, null, 'switchcategory');
-    $select->set_label(get_string('categories').':');
-
-    echo html_writer::start_tag('div', array('class' => 'categorypicker'));
-    echo $OUTPUT->render($select);
-    echo html_writer::end_tag('div');
-}
-
-if ($canmanage && empty($searchcriteria)) {
-    echo $OUTPUT->container_start('buttons');
-    // Print button to update this category.
-    if ($id) {
-        $url = new moodle_url('/course/editcategory.php', array('id' => $id));
-        echo $OUTPUT->single_button($url, get_string('editcategorythis'), 'get');
-    }
-
-    // Print button for creating new categories.
-    $url = new moodle_url('/course/editcategory.php', array('parent' => $id));
-    if ($id) {
-        $title = get_string('addsubcategory');
-    } else {
-        $title = get_string('addnewcategory');
-    }
-    echo $OUTPUT->single_button($url, $title, 'get');
-    echo $OUTPUT->container_end();
-}
-
-if (!empty($searchcriteria)) {
-    $courses = coursecat::get(0)->search_courses($searchcriteria, array('recursive' => true,
-        'offset' => $page * $perpage, 'limit' => $perpage, 'sort' => array('fullname' => 1)));
-    $numcourses = count($courses);
-    $totalcount = coursecat::get(0)->search_courses_count($searchcriteria, array('recursive' => true));
-} else 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..
-    if (has_capability('moodle/category:viewhiddencategories', $context)) {
-        $categorywhere = '';
-    } else {
-        $categorywhere = 'AND cc.visible = 1';
-    }
-    // We're going to preload the context for the subcategory as we know that we
-    // need it later on for formatting.
-    $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
-    $sql = "SELECT cc.*, $ctxselect
-              FROM {course_categories} cc
-              JOIN {context} ctx ON cc.id = ctx.instanceid
-             WHERE cc.parent = :parentid AND
-                   ctx.contextlevel = :contextlevel
-                   $categorywhere
-          ORDER BY cc.sortorder ASC";
-    $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(
-        'border' => '0',
-        'cellspacing' => '2',
-        'cellpadding' => '4',
-        'class' => 'generalbox boxaligncenter category_subcategories'
-    );
-    $table->head = array(new lang_string('subcategories'));
-    $table->data = array();
-    $baseurl = new moodle_url('/course/manage.php');
-    foreach ($subcategories as $subcategory) {
-        // Preload the context we will need it to format the category name shortly.
-        context_helper::preload_from_record($subcategory);
-        $context = context_coursecat::instance($subcategory->id);
-        // Prepare the things we need to create a link to the subcategory.
-        $attributes = $subcategory->visible ? array() : array('class' => 'dimmed');
-        $text = format_string($subcategory->name, true, array('context' => $context));
-        // Add the subcategory to the table.
-        $baseurl->param('categoryid', $subcategory->id);
-        $table->data[] = array(html_writer::link($baseurl, $text, $attributes));
+        redirect(new moodle_url($murl, array(
+            'action' => 'bulkaction',
+            'bulkmovecourses' => true,
+            'movecoursesto' => $moveto,
+            'bc' => $courses
+        )));
     }
 
-    $subcategorieswereshown = (count($table->data) > 0);
-    if ($subcategorieswereshown) {
-        echo html_writer::table($table);
-    }
-
-    $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);
-} else {
-    $subcategorieswereshown = true;
-    $courses = array();
-    $numcourses = $totalcount = 0;
-}
-
-if (!$courses) {
-    // There is no course to display.
-    if (empty($subcategorieswereshown)) {
-        echo $OUTPUT->heading(get_string("nocoursesyet"));
-    }
-} else {
-    // Display a basic list of courses with paging/editing options.
-    $table = new html_table;
-    $table->attributes = array('border' => 0, 'cellspacing' => 0, 'cellpadding' => '4', 'class' => 'generalbox boxaligncenter');
-    $table->head = array(
-        get_string('courses'),
-        get_string('edit'),
-        get_string('select')
-    );
-    $table->colclasses = array(null, null, 'mdl-align');
-    if (!empty($searchcriteria)) {
-        // add 'Category' column
-        array_splice($table->head, 1, 0, array(get_string('category')));
-        array_splice($table->colclasses, 1, 0, array(null));
-    }
-    $table->data = array();
-
-    $count = 0;
-    $abletomovecourses = false;
-
-    // Checking if we are at the first or at the last page, to allow courses to
-    // be moved up and down beyond the paging border.
-    if ($totalcount > $perpage) {
-        $atfirstpage = ($page == 0);
-        if ($perpage > 0) {
-            $atlastpage = (($page + 1) == ceil($totalcount / $perpage));
+    if (!empty($hide) or !empty($show)) {
+        // Redirect to the new management script.
+        if (!empty($hide)) {
+            redirect(new moodle_url($murl, array('courseid' => $hide, 'action' => 'hidecourse')));
         } else {
-            $atlastpage = true;
+            redirect(new moodle_url($murl, array('courseid' => $show, 'action' => 'showcourse')));
         }
-    } else {
-        $atfirstpage = true;
-        $atlastpage = true;
     }
 
-    $baseurl = new moodle_url('/course/manage.php', $urlparams + array('sesskey' => sesskey()));
-    foreach ($courses as $acourse) {
-        $coursecontext = context_course::instance($acourse->id);
-
-        $count++;
-        $up = ($count > 1 || !$atfirstpage);
-        $down = ($count < $numcourses || !$atlastpage);
-
-        $courseurl = new moodle_url('/course/view.php', array('id' => $acourse->id));
-        $attributes = array();
-        $attributes['class'] = $acourse->visible ? '' : 'dimmed';
-        $coursename = get_course_display_name_for_list($acourse);
-        $coursename = format_string($coursename, true, array('context' => $coursecontext));
-        $coursename = html_writer::link($courseurl, $coursename, $attributes);
-
-        $icons = array();
-        // Update course icon.
-        if (has_capability('moodle/course:update', $coursecontext)) {
-            $url = new moodle_url('/course/edit.php', array('id' => $acourse->id, 'category' => $id, 'returnto' => 'catmanage'));
-            $icons[] = $OUTPUT->action_icon($url, new pix_icon('t/edit', get_string('settings')));
-        }
-
-        // Role assignment icon.
-        if (has_capability('moodle/course:enrolreview', $coursecontext)) {
-            $url = new moodle_url('/enrol/users.php', array('id' => $acourse->id));
-            $icons[] = $OUTPUT->action_icon($url, new pix_icon('t/enrolusers', get_string('enrolledusers', 'enrol')));
-        }
-
-        // Delete course icon.
-        if (can_delete_course($acourse->id)) {
-            $url = new moodle_url('/course/delete.php', array('id' => $acourse->id));
-            $icons[] = $OUTPUT->action_icon($url, new pix_icon('t/delete', get_string('delete')));
-        }
-
-        // Change visibility.
-        // Users with no capability to view hidden courses, should not be able to lock themselves out.
-        if (has_any_capability(array('moodle/course:visibility', 'moodle/course:viewhiddencourses'), $coursecontext)) {
-            if (!empty($acourse->visible)) {
-                $url = new moodle_url($baseurl, array('hide' => $acourse->id));
-                $icons[] = $OUTPUT->action_icon($url, new pix_icon('t/hide', get_string('hide')));
-            } else {
-                $url = new moodle_url($baseurl, array('show' => $acourse->id));
-                $icons[] = $OUTPUT->action_icon($url, new pix_icon('t/show', get_string('show')));
-            }
-        }
-
-        // Backup course icon.
-        if (has_capability('moodle/backup:backupcourse', $coursecontext)) {
-            $url = new moodle_url('/backup/backup.php', array('id' => $acourse->id));
-            $icons[] = $OUTPUT->action_icon($url, new pix_icon('t/backup', get_string('backup')));
-        }
-
-        // Restore course icon.
-        if (has_capability('moodle/restore:restorecourse', $coursecontext)) {
-            $url = new moodle_url('/backup/restorefile.php', array('contextid' => $coursecontext->id));
-            $icons[] = $OUTPUT->action_icon($url, new pix_icon('t/restore', get_string('restore')));
-        }
-
-        if ($canmanage) {
-            if ($up && empty($searchcriteria)) {
-                $url = new moodle_url($baseurl, array('moveup' => $acourse->id));
-                $icons[] = $OUTPUT->action_icon($url, new pix_icon('t/up', get_string('moveup')));
-            }
-            if ($down && empty($searchcriteria)) {
-                $url = new moodle_url($baseurl, array('movedown' => $acourse->id));
-                $icons[] = $OUTPUT->action_icon($url, new pix_icon('t/down', get_string('movedown')));
-            }
-            $abletomovecourses = true;
-        }
-
-        $table->data[] = new html_table_row(array(
-            new html_table_cell($coursename),
-            new html_table_cell(join('', $icons)),
-            new html_table_cell(html_writer::empty_tag('input', array('type' => 'checkbox', 'name' => 'c'.$acourse->id)))
-        ));
-
-        if (!empty($searchcriteria)) {
-            // add 'Category' column
-            $category = coursecat::get($acourse->category, IGNORE_MISSING, true);
-            $cell = new html_table_cell($category->get_formatted_name());
-            $cell->attributes['class'] = $category->visible ? '' : 'dimmed_text';
-            array_splice($table->data[count($table->data) - 1]->cells, 1, 0, array($cell));
+    if (!empty($moveup) or !empty($movedown)) {
+        // Redirect to the new management script.
+        if (!empty($moveup)) {
+            redirect(new moodle_url($murl, array('courseid' => $moveup, 'action' => 'movecourseup')));
+        } else {
+            redirect(new moodle_url($murl, array('courseid' => $movedown, 'action' => 'movecoursedown')));
         }
     }
-
-    if ($abletomovecourses) {
-        $movetocategories = coursecat::make_categories_list('moodle/category:manage');
-        $movetocategories[$id] = get_string('moveselectedcoursesto');
-
-        $cell = new html_table_cell();
-        $cell->colspan = 3;
-        $cell->attributes['class'] = 'mdl-right';
-        $cell->text = html_writer::label(get_string('moveselectedcoursesto'), 'movetoid', false, array('class' => 'accesshide'));
-        $cell->text .= html_writer::select($movetocategories, 'moveto', $id, null, array('id' => 'movetoid', 'class' => 'autosubmit'));
-        $cell->text .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'categoryid', 'value' => $id));
-        $PAGE->requires->yui_module('moodle-core-formautosubmit',
-            'M.core.init_formautosubmit',
-            array(array('selectid' => 'movetoid', 'nothing' => $id))
-        );
-        $table->data[] = new html_table_row(array($cell));
-    }
-
-    $actionurl = new moodle_url('/course/manage.php');
-    $pagingurl = new moodle_url('/course/manage.php', array('categoryid' => $id, 'perpage' => $perpage) + $searchcriteria);
-
-    echo $OUTPUT->paging_bar($totalcount, $page, $perpage, $pagingurl);
-    echo html_writer::start_tag('form', array('id' => 'movecourses', 'action' => $actionurl, 'method' => 'post'));
-    echo html_writer::start_tag('div');
-    echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
-    foreach ($searchcriteria as $key => $value) {
-        echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => $key, 'value' => $value));
-    }
-    echo html_writer::table($table);
-    echo html_writer::end_tag('div');
-    echo html_writer::end_tag('form');
-    echo html_writer::empty_tag('br');
-}
-
-echo html_writer::start_tag('div', array('class' => 'buttons'));
-if ($canmanage and $numcourses > 1 && empty($searchcriteria)) {
-    // Print button to re-sort courses by name.
-    $url = new moodle_url('/course/manage.php', array('categoryid' => $id, 'resort' => 'name', 'sesskey' => sesskey()));
-    echo $OUTPUT->single_button($url, get_string('resortcoursesbyname'), 'get');
-}
-
-if (has_capability('moodle/course:create', $context) && empty($searchcriteria)) {
-    // Print button to create a new course.
-    $url = new moodle_url('/course/edit.php');
-    if ($coursecat->id) {
-        $url->params(array('category' => $coursecat->id, 'returnto' => 'catmanage'));
-    } else {
-        $url->params(array('category' => $CFG->defaultrequestcategory, 'returnto' => 'topcatmanage'));
-    }
-    echo $OUTPUT->single_button($url, get_string('addnewcourse'), 'get');
-}
-
-if (!empty($CFG->enablecourserequests) && $id == $CFG->defaultrequestcategory) {
-    print_course_request_buttons(context_system::instance());
 }
-echo html_writer::end_tag('div');
-
-echo $courserenderer->course_search_form();
-
-echo $OUTPUT->footer();
-
-/**
- * Recursive function to print all the categories ready for editing.
- *
- * @param html_table $table The table to add data to.
- * @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.
- * @param bool $displaymovecategoryto whether to display a dropdown for quick category move.
- */
-function print_category_edit(html_table $table, coursecat $category, $depth = -1, $up = false, $down = false, $displaymovecategoryto = true) {
-    global $OUTPUT;
-
-    static $str = null;
 
-    if (is_null($str)) {
-        $str = new stdClass;
-        $str->edit = new lang_string('edit');
-        $str->delete = new lang_string('delete');
-        $str->moveup = new lang_string('moveup');
-        $str->movedown = new lang_string('movedown');
-        $str->edit = new lang_string('editthiscategory');
-        $str->hide = new lang_string('hide');
-        $str->show = new lang_string('show');
-        $str->cohorts = new lang_string('cohorts', 'cohort');
-        $str->spacer = $OUTPUT->spacer().' ';
+// Now check if its a search or not. If its not we'll head to the new management page.
+$url = new moodle_url('/course/management.php');
+if ($id !== 0) {
+    // We've got an ID it can't be a search.
+    $url->param('categoryid', $id);
+} else {
+    // No $id, perhaps its a search.
+    if ($search !== '') {
+        $url->param('search', $search);
     }
-
-    if ($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('categoryid' => $category->id, 'sesskey' => sesskey()));
-        $categoryname = $category->get_formatted_name();
-        $categorypadding = str_repeat('&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;', $depth);
-        $categoryname = $categorypadding . html_writer::link($categoryurl, $categoryname, $attributes);
-
-        $icons = array();
-        if (has_capability('moodle/category:manage', $categorycontext)) {
-            // Edit category.
-            $icons[] = $OUTPUT->action_icon(
-                new moodle_url('/course/editcategory.php', array('id' => $category->id)),
-                new pix_icon('t/edit', $str->edit, 'moodle', array('class' => 'iconsmall')),
-                null, array('title' => $str->edit)
-            );
-            // Delete category.
-            $icons[] = $OUTPUT->action_icon(
-                new moodle_url('/course/manage.php', array('deletecat' => $category->id, 'sesskey' => sesskey())),
-                new pix_icon('t/delete', $str->delete, 'moodle', array('class' => 'iconsmall')),
-                null, array('title' => $str->delete)
-            );
-            // Change visibility.
-            if (!empty($category->visible)) {
-                $icons[] = $OUTPUT->action_icon(
-                    new moodle_url('/course/manage.php', array('hidecat' => $category->id, 'sesskey' => sesskey())),
-                    new pix_icon('t/hide', $str->hide, 'moodle', array('class' => 'iconsmall')),
-                    null, array('title' => $str->hide)
-                );
-            } else {
-                $icons[] = $OUTPUT->action_icon(
-                    new moodle_url('/course/manage.php', array('showcat' => $category->id, 'sesskey' => sesskey())),
-                    new pix_icon('t/show', $str->show, 'moodle', array('class' => 'iconsmall')),
-                    null, array('title' => $str->show)
-                );
-            }
-            // Cohorts.
-            if (has_any_capability(array('moodle/cohort:manage', 'moodle/cohort:view'), $categorycontext)) {
-                $icons[] = $OUTPUT->action_icon(
-                    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)
-                );
-            }
-            // Move up/down.
-            if ($up) {
-                $icons[] = $OUTPUT->action_icon(
-                    new moodle_url('/course/manage.php', array('moveupcat' => $category->id, 'sesskey' => sesskey())),
-                    new pix_icon('t/up', $str->moveup, 'moodle', array('class' => 'iconsmall')),
-                    null, array('title' => $str->moveup)
-                );
-            } else {
-                $icons[] = $str->spacer;
-            }
-            if ($down) {
-                $icons[] = $OUTPUT->action_icon(
-                    new moodle_url('/course/manage.php', array('movedowncat' => $category->id, 'sesskey' => sesskey())),
-                    new pix_icon('t/down', $str->movedown, 'moodle', array('class' => 'iconsmall')),
-                    null, array('title' => $str->movedown)
-                );
-            } else {
-                $icons[] = $str->spacer;
-            }
-        }
-
-        $actions = '';
-        if ($displaymovecategoryto && has_capability('moodle/category:manage', $categorycontext)) {
-            $popupurl = new moodle_url('/course/manage.php', array('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);
-        }
-
-        $thisrowdata = array(
-            // Category name.
-            new html_table_cell($categoryname),
-            // Course count.
-            new html_table_cell($category->coursecount),
-            // Icons.
-            new html_table_cell(join(' ', $icons)),
-        );
-        if ($displaymovecategoryto) {
-            // Actions.
-            $thisrowdata[] = new html_table_cell($actions);
-        }
-        $table->data[] = new html_table_row($thisrowdata);
+    if ($blocklist !== 0) {
+        $url->param('blocklist', $blocklist);
     }
-
-    if ($categories = $category->get_children()) {
-        // Print all the children recursively.
-        $countcats = count($categories);
-        $count = 0;
-        $first = true;
-        $last = false;
-        foreach ($categories as $cat) {
-            $count++;
-            if ($count == $countcats) {
-                $last = true;
-            }
-            $up = $first ? false : true;
-            $down = $last ? false : true;
-            $first = false;
-
-            print_category_edit($table, $cat, $depth+1, $up, $down, $displaymovecategoryto);
-        }
+    if ($modulelist !== '') {
+        $url->param('modulelist', $modulelist);
     }
 }
+redirect($url);
diff --git a/course/management.php b/course/management.php
new file mode 100644 (file)
index 0000000..c470875
--- /dev/null
@@ -0,0 +1,438 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Course and category management interfaces.
+ *
+ * @package    core_course
+ * @copyright  2013 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once('../config.php');
+require_once($CFG->dirroot.'/lib/coursecatlib.php');
+require_once($CFG->dirroot.'/course/lib.php');
+
+$categoryid = optional_param('categoryid', null, PARAM_INT);
+$courseid = optional_param('courseid', null, PARAM_INT);
+$action = optional_param('action', false, PARAM_ALPHA);
+$page = optional_param('page', 0, PARAM_INT);
+$perpage = optional_param('perpage', null, PARAM_INT);
+$viewmode = optional_param('view', 'default', PARAM_ALPHA); // Can be one of default, combined, courses, or categories.
+
+// Search related params.
+$search = optional_param('search', '', PARAM_RAW); // Search words. Shortname, fullname, idnumber and summary get searched.
+$blocklist = optional_param('blocklist', 0, PARAM_INT); // Find courses containing this block.
+$modulelist = optional_param('modulelist', '', PARAM_PLUGIN); // Find courses containing the given modules.
+
+if (!in_array($viewmode, array('default', 'combined', 'courses', 'categories'))) {
+    $viewmode = 'default';
+}
+
+$issearching = ($search !== '' || $blocklist !== 0 || $modulelist !== '');
+if ($issearching) {
+    $viewmode = 'courses';
+}
+
+$url = new moodle_url('/course/management.php');
+navigation_node::override_active_url($url);
+$systemcontext = $context = context_system::instance();
+if ($courseid) {
+    $record = get_course($courseid);
+    $course = new course_in_list($record);
+    $category = coursecat::get($course->category);
+    $categoryid = $category->id;
+    $context = context_coursecat::instance($category->id);
+    $url->param('courseid', $course->id);
+} else if ($categoryid) {
+    $courseid = null;
+    $course = null;
+    $category = coursecat::get($categoryid);
+    $context = context_coursecat::instance($category->id);
+    $url->param('categoryid', $category->id);
+} else {
+    $course = null;
+    $courseid = null;
+    $category = null;
+    $categoryid = null;
+    if ($viewmode === 'default') {
+        $viewmode = 'categories';
+    }
+    $context = $systemcontext;
+}
+
+if ($page !== 0) {
+    $url->param('page', $page);
+}
+if ($viewmode !== 'default') {
+    $url->param('view', $viewmode);
+}
+if ($search !== '') {
+    $url->param('search', $search);
+}
+if ($blocklist !== 0) {
+    $url->param('blocklist', $search);
+}
+if ($modulelist !== '') {
+    $url->param('modulelist', $search);
+}
+
+$title = format_string($SITE->fullname, true, array('context' => $systemcontext));
+
+$PAGE->set_context($context);
+$PAGE->set_url($url);
+$PAGE->set_pagelayout('admin');
+$PAGE->set_title($title);
+$PAGE->set_heading($title);
+
+if (!coursecat::has_capability_on_any(array('moodle/category:manage', 'moodle/course:create'))) {
+    // The user isn't able to manage any categories. Lets redirect them to the relevant course/index.php page.
+    $url = new moodle_url('/course/index.php');
+    if ($categoryid) {
+        $url->param('categoryid', $categoryid);
+    }
+    redirect($url);
+}
+
+// If the user poses any of these capabilities then they will be able to see the admin
+// tree and the management link within it.
+// This is the most accurate form of navigation.
+$capabilities = array(
+    'moodle/site:config',
+    'moodle/backup:backupcourse',
+    'moodle/category:manage',
+    'moodle/course:create',
+    'moodle/site:approvecourse'
+);
+if ($category && !has_any_capability($capabilities, $systemcontext)) {
+    // If the user doesn't poses any of these system capabilities then we're going to mark the manage link in the settings block
+    // as active, tell the page to ignore the active path and just build what the user would expect.
+    // This will at least give the page some relevant navigation.
+    navigation_node::override_active_url(new moodle_url('/course/management.php', array('categoryid' => $category->id)));
+    $PAGE->set_category_by_id($category->id);
+    $PAGE->navbar->ignore_active(true);
+    $PAGE->navbar->add(get_string('coursemgmt', 'admin'), $PAGE->url->out_omit_querystring());
+}
+if (!$issearching && $category !== null) {
+    $parents = coursecat::get_many($category->get_parents());
+    $parents[] = $category;
+    foreach ($parents as $parent) {
+        $PAGE->navbar->add(
+            $parent->get_formatted_name(),
+            new moodle_url('/course/management.php', array('categoryid' => $parent->id))
+        );
+    }
+    if ($course instanceof course_in_list) {
+        // Use the list name so that it matches whats being displayed below.
+        $PAGE->navbar->add($course->get_formatted_name());
+    }
+}
+
+// This is a system level page that operates on other contexts.
+require_login();
+
+$notificationspass = array();
+$notificationsfail = array();
+
+if ($action !== false && confirm_sesskey()) {
+    // Actions:
+    // - resortcategories : Resort the courses in the given category.
+    // - resortcourses : Resort courses
+    // - showcourse : make a course visible.
+    // - hidecourse : make a course hidden.
+    // - movecourseup : move the selected course up one.
+    // - movecoursedown : move the selected course down.
+    // - showcategory : make a category visible.
+    // - hidecategory : make a category hidden.
+    // - movecategoryup : move category up.
+    // - movecategorydown : move category down.
+    // - deletecategory : delete the category either in full, or moving contents.
+    // - bulkaction : performs bulk actions:
+    //    - bulkmovecourses.
+    //    - bulkmovecategories.
+    //    - bulkresortcategories.
+    $redirectback = false;
+    switch ($action) {
+        case 'resortcategories' :
+            $sort = required_param('resort', PARAM_ALPHA);
+            $cattosort = coursecat::get((int)optional_param('categoryid', 0, PARAM_INT));
+            $redirectback = \core_course\management\helper::action_category_resort_subcategories($cattosort, $sort);
+            break;
+        case 'resortcourses' :
+            // They must have specified a category.
+            required_param('categoryid', PARAM_INT);
+            $sort = required_param('resort', PARAM_ALPHA);
+            \core_course\management\helper::action_category_resort_courses($category, $sort);
+            break;
+        case 'showcourse' :
+            $redirectback = \core_course\management\helper::action_course_show($course);
+            break;
+        case 'hidecourse' :
+            $redirectback = \core_course\management\helper::action_course_hide($course);
+            break;
+        case 'movecourseup' :
+            // They must have specified a category and a course.
+            required_param('categoryid', PARAM_INT);
+            required_param('courseid', PARAM_INT);
+            $redirectback = \core_course\management\helper::action_course_change_sortorder_up_one($course, $category);
+            break;
+        case 'movecoursedown' :
+            // They must have specified a category and a course.
+            required_param('categoryid', PARAM_INT);
+            required_param('courseid', PARAM_INT);
+            $redirectback = \core_course\management\helper::action_course_change_sortorder_down_one($course, $category);
+            break;
+        case 'showcategory' :
+            // They must have specified a category.
+            required_param('categoryid', PARAM_INT);
+            $redirectback = \core_course\management\helper::action_category_show($category);
+            break;
+        case 'hidecategory' :
+            // They must have specified a category.
+            required_param('categoryid', PARAM_INT);
+            $redirectback = \core_course\management\helper::action_category_hide($category);
+            break;
+        case 'movecategoryup' :
+            // They must have specified a category.
+            required_param('categoryid', PARAM_INT);
+            $redirectback = \core_course\management\helper::action_category_change_sortorder_up_one($category);
+            break;
+        case 'movecategorydown' :
+            // They must have specified a category.
+            required_param('categoryid', PARAM_INT);
+            $redirectback = \core_course\management\helper::action_category_change_sortorder_down_one($category);
+            break;
+        case 'deletecategory':
+            // They must have specified a category.
+            required_param('categoryid', PARAM_INT);
+            if (!$category->can_delete()) {
+                throw new moodle_exception('permissiondenied', 'error', '', null, 'coursecat::can_resort');
+            }
+            require_once($CFG->dirroot.'/course/delete_category_form.php');
+            $mform = new core_course_deletecategory_form(null, $category);
+            if ($mform->is_cancelled()) {
+                redirect($PAGE->url);
+            }
+            // Start output.
+            /* @var core_course_management_renderer|core_renderer $renderer */
+            $renderer = $PAGE->get_renderer('core_course', 'management');
+            echo $renderer->header();
+            echo $renderer->heading(get_string('deletecategory', 'moodle', $category->get_formatted_name()));
+
+            if ($data = $mform->get_data()) {
+                // The form has been submit handle it.
+                if ($data->fulldelete == 1 && $category->can_delete_full()) {
+                    $continueurl = new moodle_url('/course/management.php');
+                    if ($category->parent != '0') {
+                        $continueurl->param('categoryid', $category->parent);
+                    }
+                    $notification = get_string('coursecategorydeleted', '', $category->get_formatted_name());
+                    $deletedcourses = $category->delete_full(true);
+                    foreach ($deletedcourses as $course) {
+                        echo $renderer->notification(get_string('coursedeleted', '', $course->shortname), 'notifysuccess');
+                    }
+                    echo $renderer->notification($notification, 'notifysuccess');
+                    echo $renderer->continue_button($continueurl);
+                } else if ($data->fulldelete == 0 && $category->can_move_content_to($data->newparent)) {
+                    $continueurl = new moodle_url('/course/management.php', array('categoryid' => $data->newparent));
+                    $category->delete_move($data->newparent, true);
+                    echo $renderer->continue_button($continueurl);
+                } else {
+                    // Some error in parameters (user is cheating?)
+                    $mform->display();
+                }
+            } else {
+                // Display the form.
+                $mform->display();
+            }
+            // Finish output and exit.
+            echo $renderer->footer();
+            exit();
+            break;
+        case 'bulkaction':
+            $bulkmovecourses = optional_param('bulkmovecourses', false, PARAM_BOOL);
+            $bulkmovecategories = optional_param('bulkmovecategories', false, PARAM_BOOL);
+            $bulkresortcategories = optional_param('bulkresortcategories', false, PARAM_BOOL);
+
+            if ($bulkmovecourses) {
+                // Move courses out of the current category and into a new category.
+                // They must have specified a category.
+                required_param('categoryid', PARAM_INT);
+                $movetoid = required_param('movecoursesto', PARAM_INT);
+                $courseids = optional_param_array('bc', false, PARAM_INT);
+                if ($courseids === false) {
+                    break;
+                }
+                $moveto = coursecat::get($movetoid);
+                try {
+                    // If this fails we want to catch the exception and report it.
+                    $redirectback = \core_course\management\helper::action_category_move_courses_into($category, $moveto,
+                        $courseids);
+                } catch (moodle_exception $ex) {
+                    $redirectback = false;
+                    $notificationsfail[] = $ex->getMessage();
+                }
+            } else if ($bulkmovecategories) {
+                $categoryids = optional_param_array('bcat', false, PARAM_INT);
+                $movetocatid = required_param('movecategoriesto', PARAM_INT);
+                $movetocat = coursecat::get($movetocatid);
+                $movecount = 0;
+                foreach ($categoryids as $id) {
+                    $cattomove = coursecat::get($id);
+                    if ($id == $movetocatid) {
+                        $notificationsfail[] = get_string('movecategoryownparent', 'error', $cattomove->get_formatted_name());
+                        continue;
+                    }
+                    if (strpos($movetocat->path, $cattomove->path) === 0) {
+                        $notificationsfail[] = get_string('movecategoryparentconflict', 'error', $cattomove->get_formatted_name());
+                        continue;
+                    }
+                    if ($cattomove->parent != $movetocatid) {
+                        if ($cattomove->can_change_parent($movetocatid)) {
+                            $cattomove->change_parent($movetocatid);
+                            $movecount++;
+                        } else {
+                            $notificationsfail[] = get_string('movecategorynotpossible', 'error', $cattomove->get_formatted_name());
+                        }
+                    }
+                }
+                if ($movecount > 1) {
+                    $a = new stdClass;
+                    $a->count = $movecount;
+                    $a->to = $movetocat->get_formatted_name();
+                    $notificationspass[] = get_string('movecategoriessuccess', 'moodle', $a);
+                } else if ($movecount === 1) {
+                    $a = new stdClass;
+                    $a->moved = $cattomove->get_formatted_name();
+                    $a->to = $movetocat->get_formatted_name();
+                    $notificationspass[] = get_string('movecategorysuccess', 'moodle', $a);
+                }
+            } else if ($bulkresortcategories) {
+                // Bulk resort selected categories.
+                $categoryids = optional_param_array('bcat', false, PARAM_INT);
+                $sort = required_param('resortcategoriesby', PARAM_ALPHA);
+                if ($categoryids === false) {
+                    break;
+                }
+                $categories = coursecat::get_many($categoryids);
+                foreach ($categories as $cat) {
+                    // Don't clean up here, we'll do it once we're all done.
+                    \core_course\management\helper::action_category_resort_subcategories($cat, $sort, false);
+                }
+                coursecat::resort_categories_cleanup();
+                if ($category === null && count($categoryids) === 1) {
+                    // They're bulk sorting just a single category and they've not selected a category.
+                    // Lets for convenience sake auto-select the category that has been resorted for them.
+                    redirect(new moodle_url($PAGE->url, array('categoryid' => reset($categoryids))));
+                }
+            }
+    }
+    if ($redirectback) {
+        redirect($PAGE->url);
+    }
+}
+
+if (!is_null($perpage)) {
+    set_user_preference('coursecat_management_perpage', $perpage);
+} else {
+    $perpage = get_user_preferences('coursecat_management_perpage', $CFG->coursesperpage);
+}
+if ((int)$perpage != $perpage || $perpage < 2) {
+    $perpage = $CFG->coursesperpage;
+}
+
+$categorysize = 4;
+$coursesize = 4;
+$detailssize = 4;
+if ($viewmode === 'default' || $viewmode === 'combined') {
+    if (isset($courseid)) {
+        $class = 'columns-3';
+    } else {
+        $categorysize = 5;
+        $coursesize = 7;
+        $class = 'columns-2';
+    }
+} else if ($viewmode === 'categories') {
+    $categorysize = 12;
+    $class = 'columns-1';
+} else if ($viewmode === 'courses') {
+    if (isset($courseid)) {
+        $coursesize = 6;
+        $detailssize = 6;
+        $class = 'columns-2';
+    } else {
+        $coursesize = 12;
+        $class = 'columns-1';
+    }
+}
+if ($viewmode === 'default' || $viewmode === 'combined') {
+    $class .= ' viewmode-cobmined';
+} else {
+    $class .= ' viewmode-'.$viewmode;
+}
+if (($viewmode === 'default' || $viewmode === 'combined' || $viewmode === 'courses') && !empty($courseid)) {
+    $class .= ' course-selected';
+}
+
+/* @var core_course_management_renderer|core_renderer $renderer */
+$renderer = $PAGE->get_renderer('core_course', 'management');
+$renderer->enhance_management_interface();
+
+echo $renderer->header();
+
+if (!$issearching) {
+    echo $renderer->management_heading(new lang_string('coursecatmanagement'), $viewmode, $categoryid);
+} else {
+    echo $renderer->management_heading(new lang_string('searchresults'));
+}
+
+if (count($notificationspass) > 0) {
+    echo $renderer->notification(join('<br />', $notificationspass), 'notifysuccess');
+}
+if (count($notificationsfail) > 0) {
+    echo $renderer->notification(join('<br />', $notificationsfail));
+}
+
+// Start the management form.
+echo $renderer->management_form_start();
+
+echo $renderer->grid_start('course-category-listings', $class);
+if ($viewmode === 'default' || $viewmode === 'combined' || $viewmode === 'categories') {
+    echo $renderer->grid_column_start($categorysize, 'category-listing');
+    echo $renderer->category_listing($category);
+    echo $renderer->grid_column_end();
+}
+if ($viewmode === 'default' || $viewmode === 'combined' || $viewmode === 'courses') {
+    echo $renderer->grid_column_start($coursesize, 'course-listing');
+    if (!$issearching) {
+        echo $renderer->course_listing($category, $course, $page, $perpage);
+    } else {
+        list($courses, $coursescount, $coursestotal) =
+            \core_course\management\helper::search_courses($search, $blocklist, $modulelist, $page, $perpage);
+        echo $renderer->search_listing($courses, $coursestotal, $course, $page, $perpage);
+    }
+    echo $renderer->grid_column_end();
+    if (isset($courseid)) {
+        echo $renderer->grid_column_start($detailssize, 'course-detail');
+        echo $renderer->course_detail($course);
+        echo $renderer->grid_column_end();
+    }
+}
+echo $renderer->grid_end();
+
+// End of the management form.
+echo $renderer->management_form_end();
+echo $renderer->footer();
\ No newline at end of file
index 5b489bf..b398be4 100644 (file)
@@ -55,7 +55,12 @@ abstract class moodleform_mod extends moodleform {
      */
     protected $applyadminlockedflags = false;
 
+    /** @var object The course format of the current course. */
+    protected $courseformat;
+
     function moodleform_mod($current, $section, $cm, $course) {
+        global $CFG;
+
         $this->current   = $current;
         $this->_instance = $current->instance;
         $this->_section  = $section;
@@ -66,6 +71,10 @@ abstract class moodleform_mod extends moodleform {
             $this->context = context_course::instance($course->id);
         }
 
+        // Set the course format.
+        require_once($CFG->dirroot . '/course/format/lib.php');
+        $this->courseformat = course_get_format($course);
+
         // Guess module name
         $matches = array();
         if (!preg_match('/^mod_([^_]+)_mod_form$/', get_class($this), $matches)) {
@@ -831,9 +840,9 @@ abstract class moodleform_mod extends moodleform {
             $mform->addRule('introeditor', get_string('required'), 'required', null, 'client');
         }
 
-        // If the 'show description' feature is enabled, this checkbox appears
-        // below the intro.
-        if ($this->_features->showdescription) {
+        // If the 'show description' feature is enabled, this checkbox appears below the intro.
+        // We want to hide that when using the singleactivity course format because it is confusing.
+        if ($this->_features->showdescription  && $this->courseformat->has_view_page()) {
             $mform->addElement('checkbox', 'showdescription', get_string('showdescription'));
             $mform->addHelpButton('showdescription', 'showdescription');
         }
@@ -861,7 +870,9 @@ abstract class moodleform_mod extends moodleform {
         // elements in a row need a group
         $buttonarray = array();
 
-        if ($submit2label !== false) {
+        // Label for the submit button to return to the course.
+        // Ignore this button in single activity format because it is confusing.
+        if ($submit2label !== false && $this->courseformat->has_view_page()) {
             $buttonarray[] = &$mform->createElement('submit', 'submitbutton2', $submit2label);
         }
 
index 7f7d789..c1a464e 100644 (file)
@@ -1613,8 +1613,7 @@ class core_course_renderer extends plugin_renderer_base {
         if (!$coursecat->id) {
             if (can_edit_in_category()) {
                 // add 'Manage' button instead of course search form
-                $managebutton = $this->single_button(new moodle_url('/course/manage.php'),
-                                get_string('managecourses'), 'get');
+                $managebutton = $this->single_button(new moodle_url('/course/management.php'), get_string('managecourses'), 'get');
                 $this->page->set_button($managebutton);
             }
             if (coursecat::count_all() == 1) {
index bf6fefc..75ce9c9 100644 (file)
@@ -27,13 +27,19 @@ require_once(dirname(__FILE__) . '/../config.php');
 require_once($CFG->dirroot . '/course/lib.php');
 require_once($CFG->dirroot . '/course/request_form.php');
 
-$PAGE->set_url('/course/request.php');
-
-/// Where we came from. Used in a number of redirects.
-$returnurl = $CFG->wwwroot . '/course/index.php';
+// Where we came from. Used in a number of redirects.
+$url = new moodle_url('/course/request.php');
+$return = optional_param('return', null, PARAM_ALPHANUMEXT);
+if ($return === 'management') {
+    $url->param('return', $return);
+    $returnurl = new moodle_url('/course/management.php', array('categoryid' => $CFG->defaultrequestcategory));
+} else {
+    $returnurl = new moodle_url('/course/index.php');
+}
 
+$PAGE->set_url($url);
 
-/// Check permissions.
+// Check permissions.
 require_login();
 if (isguestuser()) {
     print_error('guestsarenotallowed', '', $returnurl);
@@ -45,23 +51,23 @@ $context = context_system::instance();
 $PAGE->set_context($context);
 require_capability('moodle/course:request', $context);
 
-/// Set up the form.
+// Set up the form.
 $data = course_request::prepare();
-$requestform = new course_request_form($CFG->wwwroot . '/course/request.php', compact('editoroptions'));
+$requestform = new course_request_form($url, compact('editoroptions'));
 $requestform->set_data($data);
 
 $strtitle = get_string('courserequest');
 $PAGE->set_title($strtitle);
 $PAGE->set_heading($strtitle);
 
-/// Standard form processing if statement.
+// Standard form processing if statement.
 if ($requestform->is_cancelled()){
     redirect($returnurl);
 
 } else if ($data = $requestform->get_data()) {
     $request = course_request::create($data);
 
-    // and redirect back to the course listing.
+    // And redirect back to the course listing.
     notice(get_string('courserequestsuccess'), $returnurl);
 }
 
index 1521f53..0107982 100644 (file)
@@ -16,8 +16,7 @@
 
 /**
  * Displays external information about a course
- * @package    core
- * @category   course
+ * @package    core_course
  * @copyright  1999 onwards Martin Dougiamas  http://dougiamas.com
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
@@ -87,7 +86,7 @@ if (empty($searchcriteria)) {
     $PAGE->set_title("$site->fullname : $strsearchresults");
     // Link to manage search results should be visible if user have system or category level capability
     if ((can_edit_in_category() || !empty($usercatlist))) {
-        $aurl = new moodle_url('/course/manage.php', $searchcriteria);
+        $aurl = new moodle_url('/course/management.php', $searchcriteria);
         $searchform = $OUTPUT->single_button($aurl, get_string('managecourses'), 'get');
     } else {
         $searchform = $courserenderer->course_search_form($search, 'navbar');
index 35e9164..d8b3ab8 100644 (file)
@@ -64,11 +64,15 @@ class behat_course extends behat_base {
      *
      * @Given /^I create a course with:$/
      * @param TableNode $table The course data
+     * @return Given[]
      */
     public function i_create_a_course_with(TableNode $table) {
         return array(
             new Given('I go to the courses management page'),
-            new Given('I press "' . get_string('addnewcourse') . '"'),
+            new Given('I should see the "'.get_string('categories').'" management page'),
+            new Given('I click on category "'.get_string('miscellaneous').'" in the management interface'),
+            new Given('I should see the "'.get_string('categoriesandcoures').'" management page'),
+            new Given('I click on "'.get_string('newcourse').'" "link" in the "#course-listing" "css_element"'),
             new Given('I fill the moodle form with:', $table),
             new Given('I press "' . get_string('savechanges') . '"')
         );
@@ -78,6 +82,7 @@ class behat_course extends behat_base {
      * Goes to the system courses/categories management page.
      *
      * @Given /^I go to the courses management page$/
+     * @return Given[]
      */
     public function i_go_to_the_courses_management_page() {
 
@@ -96,6 +101,7 @@ class behat_course extends behat_base {
      * @param string $activity The activity name
      * @param int $section The section number
      * @param TableNode $data The activity field/value data
+     * @return Given[]
      */
     public function i_add_to_section_and_i_fill_the_form_with($activity, $section, TableNode $data) {
 
@@ -157,6 +163,7 @@ class behat_course extends behat_base {
      *
      * @Given /^I turn section "(?P<section_number>\d+)" highlighting on$/
      * @param int $sectionnumber The section number
+     * @return Given[]
      */
     public function i_turn_section_highlighting_on($sectionnumber) {
 
@@ -174,6 +181,7 @@ class behat_course extends behat_base {
      *
      * @Given /^I turn section "(?P<section_number>\d+)" highlighting off$/
      * @param int $sectionnumber The section number
+     * @return Given[]
      */
     public function i_turn_section_highlighting_off($sectionnumber) {
 
@@ -379,6 +387,7 @@ class behat_course extends behat_base {
      *
      * @Then /^"(?P<activity_or_resource_string>(?:[^"]|\\")*)" activity should be visible$/
      * @param string $activityname
+     * @throws ExpectationException
      */
     public function activity_should_be_visible($activityname) {
 
@@ -406,6 +415,7 @@ class behat_course extends behat_base {
      *
      * @Then /^"(?P<activity_or_resource_string>(?:[^"]|\\")*)" activity should be hidden$/
      * @param string $activityname
+     * @throws ExpectationException
      */
     public function activity_should_be_hidden($activityname) {
 
@@ -441,6 +451,7 @@ class behat_course extends behat_base {
      * @Given /^I move "(?P<activity_name_string>(?:[^"]|\\")*)" activity to section "(?P<section_number>\d+)"$/
      * @param string $activityname The activity name
      * @param int $sectionnumber The number of section
+     * @return Given[]
      */
     public function i_move_activity_to_section($activityname, $sectionnumber) {
 
@@ -477,6 +488,7 @@ class behat_course extends behat_base {
      * @throws DriverException Step not available when Javascript is disabled
      * @param string $activityname
      * @param string $newactivityname
+     * @return Given[]
      */
     public function i_change_activity_name_to($activityname, $newactivityname) {
 
@@ -499,6 +511,7 @@ class behat_course extends behat_base {
      *
      * @Given /^I indent right "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
      * @param string $activityname
+     * @return Given[]
      */
     public function i_indent_right_activity($activityname) {
 
@@ -521,6 +534,7 @@ class behat_course extends behat_base {
      *
      * @Given /^I indent left "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
      * @param string $activityname
+     * @return Given[]
      */
     public function i_indent_left_activity($activityname) {
 
@@ -544,6 +558,7 @@ class behat_course extends behat_base {
      *
      * @Given /^I delete "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
      * @param string $activityname
+     * @return Given[]
      */
     public function i_delete_activity($activityname) {
 
@@ -578,6 +593,7 @@ class behat_course extends behat_base {
      *
      * @Given /^I duplicate "(?P<activity_name_string>(?:[^"]|\\")*)" activity$/
      * @param string $activityname
+     * @return Given[]
      */
     public function i_duplicate_activity($activityname) {
         $steps = array();
@@ -597,6 +613,7 @@ class behat_course extends behat_base {
      * @Given /^I duplicate "(?P<activity_name_string>(?:[^"]|\\")*)" activity editing the new copy with:$/
      * @param string $activityname
      * @param TableNode $data
+     * @return Given[]
      */
     public function i_duplicate_activity_editing_the_new_copy_with($activityname, TableNode $data) {
         $steps = array();
@@ -793,4 +810,406 @@ class behat_course extends behat_base {
         return true;
     }
 
+    /**
+     * Returns the id of the category with the given idnumber.
+     *
+     * Please note that this function requires the category to exist. If it does not exist an ExpectationException is thrown.
+     *
+     * @param string $idnumber
+     * @return string
+     * @throws ExpectationException
+     */
+    protected function get_category_id($idnumber) {
+        global $DB;
+        try {
+            return $DB->get_field('course_categories', 'id', array('idnumber' => $idnumber), MUST_EXIST);
+        } catch (dml_missing_record_exception $ex) {
+            throw new ExpectationException(sprintf("There is no category in the database with the idnumber '%s'", $idnumber));
+        }
+    }
+
+    /**
+     * Returns the id of the course with the given idnumber.
+     *
+     * Please note that this function requires the category to exist. If it does not exist an ExpectationException is thrown.
+     *
+     * @param string $idnumber
+     * @return string
+     * @throws ExpectationException
+     */
+    protected function get_course_id($idnumber) {
+        global $DB;
+        try {
+            return $DB->get_field('course', 'id', array('idnumber' => $idnumber), MUST_EXIST);
+        } catch (dml_missing_record_exception $ex) {
+            throw new ExpectationException(sprintf("There is no course in the database with the idnumber '%s'", $idnumber));
+        }
+    }
+
+    /**
+     * Returns the category node from within the listing on the management page.
+     *
+     * @param string $idnumber
+     * @return \Behat\Mink\Element\NodeElement
+     */
+    protected function get_management_category_listing_node_by_idnumber($idnumber) {
+        $id = $this->get_category_id($idnumber);
+        $selector = sprintf('#category-listing .listitem-category[data-id="%d"] > div', $id);
+        return $this->find('css', $selector);
+    }
+
+    /**
+     * Returns a category node from within the management interface.
+     *
+     * @param string $name The name of the category.
+     * @return \Behat\Mink\Element\NodeElement
+     */
+    protected function get_management_category_listing_node_by_name($name) {
+        $selector = "//div[@id='category-listing']//li[contains(concat(' ', normalize-space(@class), ' '), ' listitem-category ')]//a[text()='{$name}']/ancestor::li[@data-id]";
+        return $this->find('xpath', $selector);
+    }
+
+    /**
+     * Returns a course node from within the management interface.
+     *
+     * @param string $name The name of the course.
+     * @return \Behat\Mink\Element\NodeElement
+     */
+    protected function get_management_course_listing_node_by_name($name) {
+        $selector = "//div[@id='course-listing']//li[contains(concat(' ', @class, ' '), ' listitem-course ')]//a[text()='{$name}']/ancestor::li[@data-id]";
+        return $this->find('xpath', $selector);
+    }
+
+    /**
+     * Returns the course node from within the listing on the management page.
+     *
+     * @param string $idnumber
+     * @return \Behat\Mink\Element\NodeElement
+     */
+    protected function get_management_course_listing_node_by_idnumber($idnumber) {
+        $id = $this->get_course_id($idnumber);
+        $selector = sprintf('#course-listing .listitem-course[data-id="%d"] > div', $id);
+        return $this->find('css', $selector);
+    }
+
+    /**
+     * Clicks on a category in the management interface.
+     *
+     * @Given /^I click on category "(?P<name>[^"]*)" in the management interface$/
+     * @param string $name
+     */
+    public function i_click_on_category_in_the_management_interface($name) {
+        $node = $this->get_management_category_listing_node_by_name($name);
+        $node->find('css', 'a.categoryname')->click();
+    }
+
+    /**
+     * Clicks on a course in the management interface.
+     *
+     * @Given /^I click on course "(?P<name>[^"]*)" in the management interface$/
+     * @param string $name
+     */
+    public function i_click_on_course_in_the_management_interface($name) {
+        $node = $this->get_management_course_listing_node_by_name($name);
+        $node->find('css', 'a.coursename')->click();
+    }
+
+    /**
+     * Click to expand a category revealing its sub categories within the management UI.
+     *
+     * @Given /^I click to expand category "(?P<idnumber>[^"]*)" in the management interface$/
+     * @param string $idnumber
+     */
+    public function i_click_to_expand_category_in_the_management_interface($idnumber) {
+        $categorynode = $this->get_management_category_listing_node_by_idnumber($idnumber);
+        $exception = new ExpectationException('Category "' . $idnumber . '" does not contain an expand or collapse toggle.', $this->getSession());
+        $togglenode = $this->find('css', 'a[data-action=collapse],a[data-action=expand]', $exception, $categorynode);
+        $togglenode->click();
+    }
+
+    /**
+     * Checks that a category within the management interface is visible.
+     *
+     * @Given /^category in management listing should be visible "(?P<idnumber>[^"]*)"$/
+     * @param string $idnumber
+     */
+    public function category_in_management_listing_should_be_visible($idnumber) {
+        $id = $this->get_category_id($idnumber);
+        $exception = new ExpectationException('The category '.$idnumber.' is not visible.', $this->getSession());
+        $selector = sprintf('#category-listing .listitem-category[data-id="%d"][data-visible="1"]', $id);
+        $this->find('css', $selector, $exception);
+    }
+
+    /**
+     * Checks that a category within the management interface is dimmed.
+     *
+     * @Given /^category in management listing should be dimmed "(?P<idnumber>[^"]*)"$/
+     * @param string $idnumber
+     */
+    public function category_in_management_listing_should_be_dimmed($idnumber) {
+        $id = $this->get_category_id($idnumber);
+        $selector = sprintf('#category-listing .listitem-category[data-id="%d"][data-visible="0"]', $id);
+        $exception = new ExpectationException('The category '.$idnumber.' is visible.', $this->getSession());
+        $this->find('css', $selector, $exception);
+    }
+
+    /**
+     * Checks that a course within the management interface is visible.
+     *
+     * @Given /^course in management listing should be visible "(?P<idnumber>[^"]*)"$/
+     * @param string $idnumber
+     */
+    public function course_in_management_listing_should_be_visible($idnumber) {
+        $id = $this->get_course_id($idnumber);
+        $exception = new ExpectationException('The course '.$idnumber.' is not visible.', $this->getSession());
+        $selector = sprintf('#course-listing .listitem-course[data-id="%d"][data-visible="1"]', $id);
+        $this->find('css', $selector, $exception);
+    }
+
+    /**
+     * Checks that a course within the management interface is dimmed.
+     *
+     * @Given /^course in management listing should be dimmed "(?P<idnumber>[^"]*)"$/
+     * @param string $idnumber
+     */
+    public function course_in_management_listing_should_be_dimmed($idnumber) {
+        $id = $this->get_course_id($idnumber);
+        $exception = new ExpectationException('The course '.$idnumber.' is visible.', $this->getSession());
+        $selector = sprintf('#course-listing .listitem-course[data-id="%d"][data-visible="0"]', $id);
+        $this->find('css', $selector, $exception);
+    }
+
+    /**
+     * Toggles the visibility of a course in the management UI.
+     *
+     * If it was visible it will be hidden. If it is hidden it will be made visible.
+     *
+     * @Given /^I toggle visibility of course "(?P<idnumber>[^"]*)" in management listing$/
+     * @param string $idnumber
+     */
+    public function i_toggle_visibility_of_course_in_management_listing($idnumber) {
+        $id = $this->get_course_id($idnumber);
+        $selector = sprintf('#course-listing .listitem-course[data-id="%d"][data-visible]', $id);
+        $node = $this->find('css', $selector);
+        $exception = new ExpectationException('Course listing "' . $idnumber . '" does not contain a show or hide toggle.', $this->getSession());
+        if ($node->getAttribute('data-visible') === '1') {
+            $toggle = $this->find('css', '.action-hide', $exception, $node);
+        } else {
+            $toggle = $this->find('css', '.action-show', $exception, $node);
+        }
+        $toggle->click();
+    }
+
+    /**
+     * Toggles the visibility of a category in the management UI.
+     *
+     * If it was visible it will be hidden. If it is hidden it will be made visible.
+     *
+     * @Given /^I toggle visibility of category "(?P<idnumber>[^"]*)" in management listing$/
+     */
+    public function i_toggle_visibility_of_category_in_management_listing($idnumber) {
+        $id = $this->get_category_id($idnumber);
+        $selector = sprintf('#category-listing .listitem-category[data-id="%d"][data-visible]', $id);
+        $node = $this->find('css', $selector);
+        $exception = new ExpectationException('Category listing "' . $idnumber . '" does not contain a show or hide toggle.', $this->getSession());
+        if ($node->getAttribute('data-visible') === '1') {
+            $toggle = $this->find('css', '.action-hide', $exception, $node);
+        } else {
+            $toggle = $this->find('css', '.action-show', $exception, $node);
+        }
+        $toggle->click();
+    }
+
+    /**
+     * Moves a category displayed in the management interface up or down one place.
+     *
+     * @Given /^I click to move category "(?P<idnumber>[^"]*)" (?P<direction>up|down) one$/
+     *
+     * @param string $idnumber The category idnumber
+     * @param string $direction The direction to move in, either up or down
+     */
+    public function i_click_to_move_category_by_one($idnumber, $direction) {
+        $node = $this->get_management_category_listing_node_by_idnumber($idnumber);
+        $this->user_moves_listing_by_one('category', $node, $direction);
+    }
+
+    /**
+     * Moves a course displayed in the management interface up or down one place.
+     *
+     * @Given /^I click to move course "(?P<idnumber>[^"]*)" (?P<direction>up|down) one$/
+     *
+     * @param string $idnumber The course idnumber
+     * @param string $direction The direction to move in, either up or down
+     */
+    public function i_click_to_move_course_by_one($idnumber, $direction) {
+        $node = $this->get_management_course_listing_node_by_idnumber($idnumber);
+        $this->user_moves_listing_by_one('course', $node, $direction);
+    }
+
+    /**
+     * Moves a course or category listing within the management interface up or down by one.
+     *
+     * @param string $listingtype One of course or category
+     * @param \Behat\Mink\Element\NodeElement $listingnode
+     * @param string $direction One of up or down.
+     * @param bool $highlight If set to false we don't check the node has been highlighted.
+     */
+    protected function user_moves_listing_by_one($listingtype, $listingnode, $direction, $highlight = true) {
+        $up = (strtolower($direction) === 'up');
+        if ($up) {
+            $exception = new ExpectationException($listingtype.' listing does not contain a moveup button.', $this->getSession());
+            $button = $this->find('css', 'a.action-moveup', $exception, $listingnode);
+        } else {
+            $exception = new ExpectationException($listingtype.' listing does not contain a movedown button.', $this->getSession());
+            $button = $this->find('css', 'a.action-movedown', $exception, $listingnode);
+        }
+        $button->click();
+        if ($this->running_javascript() && $highlight) {
+            $listitem = $listingnode->getParent();
+            $exception = new ExpectationException('Nothing was highlighted, ajax didn\'t occur or didn\'t succeed.', $this->getSession());
+            $this->spin(array($this, 'listing_is_highlighted'), $listitem->getTagName().'#'.$listitem->getAttribute('id'), 2, $exception, true);
+        }
+    }
+
+    /**
+     * Used by spin to determine the callback has been highlighted.
+     *
+     * @param behat_course $self A self reference (default first arg from a spin callback)
+     * @param \Behat\Mink\Element\NodeElement $selector
+     * @return bool
+     */
+    protected function listing_is_highlighted($self, $selector) {
+        $listitem = $this->find('css', $selector);
+        return $listitem->hasClass('highlight');
+    }
+
+    /**
+     * Check that one course appears before another in the course category management listings.
+     *
+     * @Given /^I should see course listing "(?P<preceedingcourse>[^"]*)" before "(?P<followingcourse>[^"]*)"$/
+     *
+     * @param string $preceedingcourse The first course to find
+     * @param string $followingcourse The second course to find (should be AFTER the first course)
+     * @throws ExpectationException
+     */
+    public function i_should_see_course_listing_before($preceedingcourse, $followingcourse) {
+        $xpath = "//div[@id='course-listing']//li[contains(concat(' ', @class, ' '), ' listitem-course ')]//a[text()='{$preceedingcourse}']/ancestor::li[@data-id]//following::a[text()='{$followingcourse}']";
+        $msg = "{$preceedingcourse} course does not appear before {$followingcourse} course";
+        if (!$this->getSession()->getDriver()->find($xpath)) {
+            throw new ExpectationException($msg, $this->getSession());
+        }
+    }
+
+    /**
+     * Check that one category appears before another in the course category management listings.
+     *
+     * @Given /^I should see category listing "(?P<preceedingcategory>[^"]*)" before "(?P<followingcategory>[^"]*)"$/
+     *
+     * @param string $preceedingcategory The first category to find
+     * @param string $followingcategory The second category to find (should be after the first category)
+     * @throws ExpectationException
+     */
+    public function i_should_see_category_listing_before($preceedingcategory, $followingcategory) {
+        $xpath = "//div[@id='category-listing']//li[contains(concat(' ', @class, ' '), ' listitem-category ')]//a[text()='{$preceedingcategory}']/ancestor::li[@data-id]//following::a[text()='{$followingcategory}']";
+        $msg = "{$preceedingcategory} category does not appear before {$followingcategory} category";
+        if (!$this->getSession()->getDriver()->find($xpath)) {
+            throw new ExpectationException($msg, $this->getSession());
+        }
+    }
+
+    /**
+     * Checks that we are on the course management page that we expect to be on and that no course has been selected.
+     *
+     * @Given /^I should see the "(?P<mode>[^"]*)" management page$/
+     * @param string $mode The mode to expected. One of 'Courses', 'Course categories' or 'Course categories and courses'
+     * @return Given[]
+     */
+    public function i_should_see_the_courses_management_page($mode) {
+        $return = array(
+            new Given('I should see "Course and category management" in the "h2" "css_element"')
+        );
+        switch ($mode) {
+            case "Courses":
+                $return[] = new Given('"#category-listing" "css_element" should not exists');
+                $return[] = new Given('"#course-listing" "css_element" should exists');
+                break;
+            case "Course categories":
+                $return[] = new Given('"#category-listing" "css_element" should exists');
+                $return[] = new Given('"#course-listing" "css_element" should not exists');
+                break;
+            case "Courses categories and courses":
+            default:
+                $return[] = new Given('"#category-listing" "css_element" should exists');
+                $return[] = new Given('"#course-listing" "css_element" should exists');
+                break;
+        }
+        $return[] = new Given('"#course-detail" "css_element" should not exists');
+        return $return;
+    }
+
+    /**
+     * Checks that we are on the course management page that we expect to be on and that a course has been selected.
+     *
+     * @Given /^I should see the "(?P<mode>[^"]*)" management page with a course selected$/
+     * @param string $mode The mode to expected. One of 'Courses', 'Course categories' or 'Course categories and courses'
+     * @return Given[]
+     */
+    public function i_should_see_the_courses_management_page_with_a_course_selected($mode) {
+        $return = $this->i_should_see_the_courses_management_page($mode);
+        array_pop($return);
+        $return[] = new Given('"#course-detail" "css_element" should exists');
+        return $return;
+    }
+
+    /**
+     * Locates a course in the course category management interface and then triggers an action for it.
+     *
+     * @Given /^I click on "(?P<action>[^"]*)" action for "(?P<name>[^"]*)" in management course listing$/
+     *
+     * @param string $action The action to take. One of
+     * @param string $name The name of the course as it is displayed in the management interface.
+     */
+    public function i_click_on_action_for_item_in_management_course_listing($action, $name) {
+        $node = $this->get_management_course_listing_node_by_name($name);
+        $this->user_clicks_on_management_listing_action('course', $node, $action);
+    }
+
+    /**
+     * Locates a category in the course category management interface and then triggers an action for it.
+     *
+     * @Given /^I click on "(?P<action>[^"]*)" action for "(?P<name>[^"]*)" in management category listing$/
+     *
+     * @param string $action The action to take. One of
+     * @param string $name The name of the category as it is displayed in the management interface.
+     */
+    public function i_click_on_action_for_item_in_management_category_listing($action, $name) {
+        $node = $this->get_management_category_listing_node_by_name($name);
+        $this->user_clicks_on_management_listing_action('category', $node, $action);
+    }
+
+    /**
+     * Finds the node to use for a management listitem action and clicks it.
+     *
+     * @param string $listingtype Either course or category.
+     * @param \Behat\Mink\Element\NodeElement $listingnode
+     * @param string $action The action being taken
+     * @throws Behat\Mink\Exception\ExpectationException
+     */
+    protected function user_clicks_on_management_listing_action($listingtype, $listingnode, $action) {
+        $actionsnode = $listingnode->find('xpath', "//*[contains(concat(' ', normalize-space(@class), ' '), '{$listingtype}-item-actions')]");
+        if (!$actionsnode) {
+            throw new ExpectationException("Could not find the actions for $listingtype", $this->getSession());
+        }
+        $actionnode = $actionsnode->find('css', '.action-'.$action);
+        if ($actionnode === null && $this->running_javascript()) {
+            $actionsnode->find('css', 'a.toggle-display')->click();
+            if ($actionnode) {
+                $actionnode = $listingnode->find('css', '.action-'.$action);
+            }
+        }
+        if (!$actionnode) {
+            throw new ExpectationException("Expected action was not available or not found ($action)", $this->getSession());
+        }
+        $actionnode->click();
+    }
 }
diff --git a/course/tests/behat/category_change_visibility.feature b/course/tests/behat/category_change_visibility.feature
new file mode 100644 (file)
index 0000000..d51aef5
--- /dev/null
@@ -0,0 +1,252 @@
+@core @core_course
+Feature: We can change the visibility of categories in the management interface.
+  As a moodle admin
+  I need to test hiding and showing a category.
+  I need to test hiding and showing a sub category.
+  I need to test visibility is applied to sub categories.
+  I need to test visibility is applied to courses.
+  I need to test visibility of children is reset when changing back.
+
+  # Tests hiding and then showing a single category.
+  Scenario: Test making a category hidden and then visible again.
+    Given the following "categories" exists:
+      | name | category | idnumber |
+      | Cat 1 | 0 | CAT1 |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I should see "Cat 1" in the "#category-listing ul.ml" "css_element"
+    And category in management listing should be visible "CAT1"
+    And I toggle visibility of category "CAT1" in management listing
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And category in management listing should be dimmed "CAT1"
+    And I toggle visibility of category "CAT1" in management listing
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And category in management listing should be visible "CAT1"
+
+  # Tests hiding and then showing a single category.
+  @javascript
+  Scenario: Test using AJAX to make a category hidden and then visible again.
+    Given the following "categories" exists:
+      | name | category | idnumber |
+      | Cat 1 | 0 | CAT1 |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I should see "Cat 1" in the "#category-listing ul.ml" "css_element"
+    And category in management listing should be visible "CAT1"
+    And I toggle visibility of category "CAT1" in management listing
+    # AJAX updated.
+    And category in management listing should be dimmed "CAT1"
+    And I toggle visibility of category "CAT1" in management listing
+    # AJAX updated.
+    And category in management listing should be visible "CAT1"
+
+  # Tests hiding and then showing a subcategory.
+  Scenario: Test making a subcategory hidden and then visible again.
+    Given the following "categories" exists:
+      | name | category | idnumber |
+      | Cat 1 | 0 | CAT1 |
+      | Cat 2 | CAT1 | CAT2 |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I should see "Cat 1" in the "#category-listing ul.ml" "css_element"
+    And I should not see "Cat 2" in the "#category-listing ul.ml" "css_element"
+    And category in management listing should be visible "CAT1"
+    And I click on category "Cat 1" in the management interface
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I should see "Cat 1" in the "#category-listing ul.ml" "css_element"
+    And I should see "Cat 2" in the "#category-listing ul.ml" "css_element"
+    And category in management listing should be visible "CAT1"
+    And category in management listing should be visible "CAT2"
+    And I toggle visibility of category "CAT2" in management listing
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I should see "Cat 1" in the "#category-listing ul.ml" "css_element"
+    And I should see "Cat 2" in the "#category-listing ul.ml" "css_element"
+    And category in management listing should be visible "CAT1"
+    And category in management listing should be dimmed "CAT2"
+    And I toggle visibility of category "CAT2" in management listing
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I should see "Cat 1" in the "#category-listing ul.ml" "css_element"
+    And I should see "Cat 2" in the "#category-listing ul.ml" "css_element"
+    And category in management listing should be visible "CAT1"
+    And category in management listing should be visible "CAT2"
+
+  # Tests hiding and then showing a subcategory.
+  @javascript
+  Scenario: Test using AJAX to make a subcategory hidden and then visible again.
+    Given the following "categories" exists:
+      | name | category | idnumber |
+      | Cat 1 | 0 | CAT1 |
+      | Cat 2 | CAT1 | CAT2 |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I should see "Cat 1" in the "#category-listing ul.ml" "css_element"
+    And I should not see "Cat 2" in the "#category-listing ul.ml" "css_element"
+    And category in management listing should be visible "CAT1"
+    And I click to expand category "CAT1" in the management interface
+    # AJAX loads sub category.
+    And category in management listing should be visible "CAT1"
+    And category in management listing should be visible "CAT2"
+    And I toggle visibility of category "CAT2" in management listing
+    # AJAX hides the subcategory.
+    And category in management listing should be visible "CAT1"
+    And category in management listing should be dimmed "CAT2"
+    And I toggle visibility of category "CAT2" in management listing
+    # AJAX reveals the subcategory.
+    And category in management listing should be visible "CAT1"
+    And category in management listing should be visible "CAT2"
+
+  # The test below this is identical except with JavaScript enabled.
+  Scenario: Test relation between category and course when changing visibility.
+    Given the following "categories" exists:
+      | name | category | idnumber |
+      | Cat 1 | 0 | CAT1 |
+      | Cat 2 | 0 | CAT2 |
+      | Cat 3 | CAT1 | CAT3 |
+      | Cat 4 | CAT1 | CAT4 |
+    And the following "courses" exists:
+      | category | fullname | shortname | idnumber |
+      | CAT1 | Course 1 | Course 1 | C1 |
+      | CAT1 | Course 2 | Course 2 | C2 |
+      | CAT1 | Course 3 | Course 3 | C3 |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I click on category "Cat 1" in the management interface
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I should see "Cat 1" in the "#category-listing ul.ml" "css_element"
+    And I should see "Cat 2" in the "#category-listing ul.ml" "css_element"
+    And I should see "Cat 3" in the "#category-listing ul.ml" "css_element"
+    And I should see "Cat 4" in the "#category-listing ul.ml" "css_element"
+    And I should see "Course 1" in the "#course-listing ul.ml" "css_element"
+    And I should see "Course 2" in the "#course-listing ul.ml" "css_element"
+    And I should see "Course 3" in the "#course-listing ul.ml" "css_element"
+    And category in management listing should be visible "CAT1"
+    And category in management listing should be visible "CAT2"
+    And category in management listing should be visible "CAT3"
+    And category in management listing should be visible "CAT4"
+    And course in management listing should be visible "C1"
+    And course in management listing should be visible "C2"
+    And course in management listing should be visible "C3"
+    And I toggle visibility of course "C2" in management listing
+    # Redirect.
+    And I should see the "Course categories and courses" management page with a course selected
+    And course in management listing should be visible "C1"
+    And course in management listing should be dimmed "C2"
+    And course in management listing should be visible "C3"
+    And I toggle visibility of category "CAT3" in management listing
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I click on "Cat 1" "link"
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And category in management listing should be visible "CAT1"
+    And category in management listing should be visible "CAT2"
+    And category in management listing should be dimmed "CAT3"
+    And category in management listing should be visible "CAT4"
+    And course in management listing should be visible "C1"
+    And course in management listing should be dimmed "C2"
+    And course in management listing should be visible "C3"
+    And I toggle visibility of category "CAT1" in management listing
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And category in management listing should be dimmed "CAT1"
+    And category in management listing should be visible "CAT2"
+    And category in management listing should be dimmed "CAT3"
+    And category in management listing should be dimmed "CAT4"
+    And course in management listing should be dimmed "C1"
+    And course in management listing should be dimmed "C2"
+    And course in management listing should be dimmed "C3"
+    And I toggle visibility of category "CAT1" in management listing
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And category in management listing should be visible "CAT1"
+    And category in management listing should be visible "CAT2"
+    And category in management listing should be dimmed "CAT3"
+    And category in management listing should be visible "CAT4"
+    And course in management listing should be visible "C1"
+    And course in management listing should be dimmed "C2"
+    And course in management listing should be visible "C3"
+
+  # The test above this is identical except without JavaScript enabled.
+  @javascript @_cross_browser
+  Scenario: Test the relation between category and course when changing visibility with AJAX
+    Given the following "categories" exists:
+      | name | category | idnumber |
+      | Cat 1 | 0 | CAT1 |
+      | Cat 2 | 0 | CAT2 |
+      | Cat 3 | CAT1 | CAT3 |
+      | Cat 4 | CAT1 | CAT4 |
+    And the following "courses" exists:
+      | category | fullname | shortname | idnumber |
+      | CAT1 | Course 1 | Course 1 | C1 |
+      | CAT1 | Course 2 | Course 2 | C2 |
+      | CAT1 | Course 3 | Course 3 | C3 |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I click on category "Cat 1" in the management interface
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I should see "Cat 1" in the "#category-listing ul.ml" "css_element"
+    And I should see "Cat 2" in the "#category-listing ul.ml" "css_element"
+    And I should see "Cat 3" in the "#category-listing ul.ml" "css_element"
+    And I should see "Cat 4" in the "#category-listing ul.ml" "css_element"
+    And I should see "Course 1" in the "#course-listing ul.ml" "css_element"
+    And I should see "Course 2" in the "#course-listing ul.ml" "css_element"
+    And I should see "Course 3" in the "#course-listing ul.ml" "css_element"
+    And category in management listing should be visible "CAT1"
+    And category in management listing should be visible "CAT2"
+    And category in management listing should be visible "CAT3"
+    And category in management listing should be visible "CAT4"
+    And course in management listing should be visible "C1"
+    And course in management listing should be visible "C2"
+    And course in management listing should be visible "C3"
+    And I toggle visibility of course "C2" in management listing
+    # AJAX action - no redirect.
+    And I should see "Cat 3" in the "#category-listing ul.ml" "css_element"
+    And course in management listing should be visible "C1"
+    And course in management listing should be dimmed "C2"
+    And course in management listing should be visible "C3"
+    And I toggle visibility of category "CAT3" in management listing
+    # AJAX action - no redirect.
+    And category in management listing should be visible "CAT1"
+    And category in management listing should be visible "CAT2"
+    And category in management listing should be dimmed "CAT3"
+    And category in management listing should be visible "CAT4"
+    And course in management listing should be visible "C1"
+    And course in management listing should be dimmed "C2"
+    And course in management listing should be visible "C3"
+    And I toggle visibility of category "CAT1" in management listing
+    # AJAX action - no redirect.
+    And category in management listing should be dimmed "CAT1"
+    And category in management listing should be visible "CAT2"
+    And category in management listing should be dimmed "CAT3"
+    And category in management listing should be dimmed "CAT4"
+    And course in management listing should be dimmed "C1"
+    And course in management listing should be dimmed "C2"
+    And course in management listing should be dimmed "C3"
+    And I toggle visibility of category "CAT1" in management listing
+    # AJAX action - no redirect.
+    And category in management listing should be visible "CAT1"
+    And category in management listing should be visible "CAT2"
+    And category in management listing should be dimmed "CAT3"
+    And category in management listing should be visible "CAT4"
+    And course in management listing should be visible "C1"
+    And course in management listing should be dimmed "C2"
+    And course in management listing should be visible "C3"
diff --git a/course/tests/behat/category_management.feature b/course/tests/behat/category_management.feature
new file mode 100644 (file)
index 0000000..5dbc4f9
--- /dev/null
@@ -0,0 +1,212 @@
+@core @core_course
+Feature: Test category management actions
+  As a moodle admin
+  Test we can create a category
+  Test we can create a sub category
+  Test we can edit a category
+  Test we can delete a category
+  Test we can assign roles within a category
+  Test we can set permissions on a category
+  Test we can manage cohorts within a category
+  Test we can manage filters for a category
+
+  Scenario: Test editing a category through the management interface.
+    Given the following "categories" exists:
+      | name | category | idnumber |
+      | Cat 1 | 0 | CAT1 |
+    And the following "courses" exists:
+      | category | fullname | shortname | idnumber |
+      | CAT1 | Course 1 | Course 1 | C1 |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I click on "edit" action for "Cat 1" in management category listing
+    # Redirect
+    And I should see "Edit category settings"
+    And I should see "Cat 1"
+    And I press "Cancel"
+    # Redirect
+    And I should see the "Course categories and courses" management page
+    And I click on "edit" action for "Cat 1" in management category listing
+    # Redirect
+    And I should see "Edit category settings"
+    And I should see "Cat 1"
+    And I fill the moodle form with:
+      | Category name | Category 1 (edited) |
+      | Category ID number | CAT1e |
+    And I press "Save changes"
+    # Redirect
+    And I should see the "Course categories and courses" management page
+    And I should see "Category 1 (edited)" in the "#category-listing" "css_element"
+    And I should see "Category 1 (edited)" in the "#course-listing h3" "css_element"
+
+  Scenario: Test deleting a categories through the management interface.
+    Given the following "categories" exists:
+      | name | category | idnumber |
+      | Cat 1 | 0 | CAT1 |
+      | Cat 2 | 0 | CAT2 |
+      | Cat 3 | 0 | CAT3 |
+
+    And the following "courses" exists:
+      | category | fullname | shortname | idnumber |
+      | CAT3 | Course 1 | Course 1 | C1 |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I should see "Cat 1" in the "#category-listing ul.ml" "css_element"
+    And I should see "Cat 2" in the "#category-listing ul.ml" "css_element"
+    And I should see "Cat 3" in the "#category-listing ul.ml" "css_element"
+    And I click on "delete" action for "Cat 2" in management category listing
+    # Redirect
+    And I should see "Delete category: Cat 2"
+    And I should see "Contents of Cat 2"
+    And I should see "This category is empty"
+    And I press "Cancel"
+    # Redirect
+    And I should see the "Course categories and courses" management page
+    And I should see "Cat 1" in the "#category-listing ul.ml" "css_element"
+    And I should see "Cat 2" in the "#category-listing ul.ml" "css_element"
+    And I should see "Cat 3" in the "#category-listing ul.ml" "css_element"
+    And I click on "delete" action for "Cat 2" in management category listing
+    # Redirect
+    And I should see "Delete category: Cat 2"
+    And I should see "Contents of Cat 2"
+    And I should see "This category is empty"
+    And I press "Delete"
+    # Redirect
+    And I should see "Delete category: Cat 2"
+    And I should see "Deleted course category Cat 2"
+    And I press "Continue"
+    # Redirect
+    And I should see the "Course categories" management page
+    And I should see "Cat 1" in the "#category-listing ul.ml" "css_element"
+    And I should not see "Cat 2" in the "#category-listing ul.ml" "css_element"
+    And I should see "Cat 3" in the "#category-listing ul.ml" "css_element"
+    And I click on "delete" action for "Cat 3" in management category listing
+    # Redirect
+    And I should see "Delete category: Cat 3"
+    And I fill the moodle form with:
+      | What to do | Move contents to another category |
+      | Move into  | Cat 1                             |
+    And I press "Delete"
+    # Redirect
+    And I should see "Delete category: Cat 3"
+    And I should see "Deleted course category Cat 3"
+    And I press "Continue"
+    # Redirect
+    And I should see the "Course categories and courses" management page
+    And I should see "Cat 1" in the "#category-listing ul.ml" "css_element"
+    And I should not see "Cat 2" in the "#category-listing ul.ml" "css_element"
+    And I should not see "Cat 3" in the "#category-listing ul.ml" "css_element"
+    And I should see "Course 1" in the "#course-listing ul.ml" "css_element"
+
+  Scenario: Test I can assign roles for a category through the management interface.
+    Given the following "categories" exists:
+      | name | category | idnumber |
+      | Cat 1 | 0 | CAT1 |
+    And the following "courses" exists:
+      | category | fullname | shortname | idnumber |
+      | CAT1 | Course 1 | Course 1 | C1 |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I click on "assignroles" action for "Cat 1" in management category listing
+    # Redirect
+    And I should see "Assign roles in Category: Cat 1"
+    And I should see "Please choose a role to assign"
+    And I click on "Back to Category: Cat 1" "link"
+    # Redirect
+    And I should see the "Course categories and courses" management page
+    And I should see "Cat 1" in the "#course-listing h3" "css_element"
+
+  Scenario: Test I can set access permissions for a category through the management interface.
+    Given the following "categories" exists:
+      | name | category | idnumber |
+      | Cat 1 | 0 | CAT1 |
+    And the following "courses" exists:
+      | category | fullname | shortname | idnumber |
+      | CAT1 | Course 1 | Course 1 | C1 |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I click on "permissions" action for "Cat 1" in management category listing
+    # Redirect
+    And I should see "Permissions in Category: Cat 1"
+    And I click on "Back to Category: Cat 1" "link"
+    # Redirect
+    And I should see the "Course categories and courses" management page
+    And I should see "Cat 1" in the "#course-listing h3" "css_element"
+
+  Scenario: Test clicking to manage cohorts for a category through the management interface.
+    Given the following "categories" exists:
+      | name | category | idnumber |
+      | Cat 1 | 0 | CAT1 |
+    And the following "courses" exists:
+      | category | fullname | shortname | idnumber |
+      | CAT1 | Course 1 | Course 1 | C1 |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I click on "cohorts" action for "Cat 1" in management category listing
+    # Redirect
+    And I should see "Category: Cat 1: available cohorts"
+
+  Scenario: Test configuring filters for a category
+    Given the following "categories" exists:
+      | name | category | idnumber |
+      | Cat 1 | 0 | CAT1 |
+    And the following "courses" exists:
+      | category | fullname | shortname | idnumber |
+      | CAT1 | Course 1 | Course 1 | C1 |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I click on "filters" action for "Cat 1" in management category listing
+    # Redirect
+    And I should see "Filter settings in Category: Cat 1"
+    And I click on "Back to Category: Cat 1" "link"
+    # Redirect
+    And I should see the "Course categories and courses" management page
+    And I should see "Cat 1" in the "#course-listing h3" "css_element"
+
+  @javascript
+  Scenario: Test that I can create a category and view it in the management interface
+    Given the following "categories" exists:
+      | name | category | idnumber |
+      | Cat 1 | 0 | CAT1 |
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I click on "Create new" "link" in the ".category-listing-actions" "css_element"
+    And I click on "Top level category" "link" in the ".category-listing-actions" "css_element"
+    # Redirect.
+    And I should see "Add new category"
+    And I fill the moodle form with:
+      | Category name | Test category 2 |
+      | Category ID number | TC2 |
+    And I press "Create category"
+    # Redirect
+    And I should see the "Course categories and courses" management page
+    And I should see "Test category 2" in the "#course-listing h3" "css_element"
+    And I should see category listing "Cat 1" before "Test category 2"
+    And I should see "No courses in this category"
+    And I click on "Create new" "link" in the ".category-listing-actions" "css_element"
+    And I click on "Sub category" "link" in the ".category-listing-actions" "css_element"
+    # Redirect
+    And I should see "Add new category"
+    And I fill the moodle form with:
+      | Category name | Test category 3 |
+      | Category ID number | TC3 |
+    And I press "Create category"
+    # Redirect
+    And I should see the "Course categories and courses" management page
+    And I should see "Test category 3" in the "#course-listing h3" "css_element"
+    And I should see category listing "Cat 1" before "Test category 2"
+    And I should see "No courses in this category"
+
diff --git a/course/tests/behat/category_resort.feature b/course/tests/behat/category_resort.feature
new file mode 100644 (file)
index 0000000..147617f
--- /dev/null
@@ -0,0 +1,215 @@
+@core @core_course
+Feature: Test we can resort categories in the management interface.
+  As a moodle admin
+  I need to test we can resort top level categories.
+  I need to test we can resort sub categories.
+  I need to test we can manually sort categories.
+
+  Scenario Outline: Test resorting categories.
+    Given the following "categories" exists:
+      | category | name | idnumber | sortorder |
+      | 0 | Social studies | Ext003 | 1 |
+      | 0 | Applied sciences | Sci001 | 2 |
+      | 0 | Extended social studies | Ext002 | 3 |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I should see "Re-sort categories" in the ".category-listing-actions" "css_element"
+    And I should see "By name" in the ".category-listing-actions" "css_element"
+    And I should see "By idnumber" in the ".category-listing-actions" "css_element"
+    And I click on <sortby> "link" in the ".category-listing-actions" "css_element"
+    # Redirect.
+    And I should see the "Course categories" management page
+    And I should see category listing <cat1> before <cat2>
+    And I should see category listing <cat2> before <cat3>
+
+  Examples:
+    | sortby | cat1 | cat2 | cat3 |
+    | "Re-sort categories" | "Social studies"          | "Applied sciences"        | "Extended social studies" |
+    | "By name"            | "Applied sciences"        | "Extended social studies" | "Social studies" |
+    | "By idnumber"        | "Extended social studies" | "Social studies" | "Applied sciences" |
+
+  @javascript
+  Scenario Outline: Test resorting categories with JS enabled.
+    Given the following "categories" exists:
+      | category | name | idnumber | sortorder |
+      | 0 | Social studies | Ext003 | 1 |
+      | 0 | Applied sciences | Sci001 | 2 |
+      | 0 | Extended social studies | Ext002 | 3 |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I should see "Re-sort categories" in the ".category-listing-actions" "css_element"
+    And I should not see "By name" in the ".category-listing-actions" "css_element"
+    And I should not see "By idnumber" in the ".category-listing-actions" "css_element"
+    And I click on "Re-sort categories" "link"
+    And I should see "By name" in the ".category-listing-actions" "css_element"
+    And I should see "By idnumber" in the ".category-listing-actions" "css_element"
+    And I click on <sortby> "link" in the ".category-listing-actions" "css_element"
+    # Redirect.
+    And I should see the "Course categories" management page
+    And I should see category listing <cat1> before <cat2>
+    And I should see category listing <cat2> before <cat3>
+
+  Examples:
+    | sortby | cat1 | cat2 | cat3 |
+    | "Re-sort categories" | "Social studies"          | "Applied sciences"        | "Extended social studies" |
+    | "By name"            | "Applied sciences"        | "Extended social studies" | "Social studies" |
+    | "By idnumber"        | "Extended social studies" | "Social studies" | "Applied sciences" |
+
+  Scenario Outline: Test resorting subcategories.
+    Given the following "categories" exists:
+      | category | name | idnumber | sortorder |
+      | 0 | Master cat  | CAT1 | 1 |
+      | CAT1 | Social studies | Ext003 | 1 |
+      | CAT1 | Applied sciences | Sci001 | 2 |
+      | CAT1 | Extended social studies | Ext002 | 3 |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I click on "Master cat" "link"
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I should see "Re-sort subcategories" in the ".category-listing-actions" "css_element"
+    And I should see "By name" in the ".category-listing-actions" "css_element"
+    And I should see "By idnumber" in the ".category-listing-actions" "css_element"
+    And I click on <sortby> "link" in the ".category-listing-actions" "css_element"
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I should see category listing <cat1> before <cat2>
+    And I should see category listing <cat2> before <cat3>
+
+  Examples:
+    | sortby | cat1 | cat2 | cat3 |
+    | "Re-sort subcategories" | "Social studies"          | "Applied sciences"        | "Extended social studies" |
+    | "By name"            | "Applied sciences"        | "Extended social studies" | "Social studies" |
+    | "By idnumber"        | "Extended social studies" | "Social studies" | "Applied sciences" |
+
+  @javascript
+  Scenario Outline: Test resorting subcategories with JS enabled.
+    Given the following "categories" exists:
+      | category | name | idnumber | sortorder |
+      | 0 | Master cat  | CAT1 | 1 |
+      | CAT1 | Social studies | Ext003 | 1 |
+      | CAT1 | Applied sciences | Sci001 | 2 |
+      | CAT1 | Extended social studies | Ext002 | 3 |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I click on "Master cat" "link"
+  # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I should see "Re-sort subcategories" in the ".category-listing-actions" "css_element"
+    And I should not see "By name" in the ".category-listing-actions" "css_element"
+    And I should not see "By idnumber" in the ".category-listing-actions" "css_element"
+    And I click on "Re-sort subcategories" "link"
+    And I should see "By name" in the ".category-listing-actions" "css_element"
+    And I should see "By idnumber" in the ".category-listing-actions" "css_element"
+    And I click on <sortby> "link" in the ".category-listing-actions" "css_element"
+  # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I should see category listing <cat1> before <cat2>
+    And I should see category listing <cat2> before <cat3>
+
+  Examples:
+    | sortby | cat1 | cat2 | cat3 |
+    | "Re-sort subcategories" | "Social studies"          | "Applied sciences"        | "Extended social studies" |
+    | "By name"            | "Applied sciences"        | "Extended social studies" | "Social studies" |
+    | "By idnumber"        | "Extended social studies" | "Social studies" | "Applied sciences" |
+
+  # The scenario below this is the same but with JS enabled.
+  Scenario: Test moving categories up and down by one.
+    Given the following "categories" exists:
+      | category | idnumber | name |
+      | 0 | CAT1 | Cat 1 |
+      | 0 | CAT2 | Cat 2 |
+      | CAT1 | CATA | Cat 1a |
+      | CAT1 | CATB | Cat 1b |
+      | CAT1 | CATC | Cat 1c |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I click on category "Cat 1" in the management interface
+    # Redirect. We should a 1, 1a, 1b, 1c, 2.
+    And I should see the "Course categories and courses" management page
+    And I should see category listing "Cat 1" before "Cat 1a"
+    And I should see category listing "Cat 1a" before "Cat 1b"
+    And I should see category listing "Cat 1b" before "Cat 1c"
+    And I should see category listing "Cat 1c" before "Cat 2"
+    And I click to move category "CATA" down one
+    # Redirect.We should a 1, 1b, 1a, 1c, 2.
+    And I should see the "Course categories and courses" management page
+    And I should see category listing "Cat 1" before "Cat 1b"
+    And I should see category listing "Cat 1b" before "Cat 1a"
+    And I should see category listing "Cat 1a" before "Cat 1c"
+    And I should see category listing "Cat 1c" before "Cat 2"
+    And I click to move category "CATC" up one
+    # Redirect. We should a 1, 1b, 1c, 1a, 2.
+    And I should see the "Course categories and courses" management page
+    And I should see category listing "Cat 1" before "Cat 1b"
+    And I should see category listing "Cat 1b" before "Cat 1c"
+    And I should see category listing "Cat 1c" before "Cat 1a"
+    And I should see category listing "Cat 1a" before "Cat 2"
+    And I click to move category "CATA" down one
+    # Redirect. We should a 1, 1b, 1c, 1a, 2.
+    And I should see the "Course categories and courses" management page
+    And I should see category listing "Cat 1" before "Cat 1b"
+    And I should see category listing "Cat 1b" before "Cat 1c"
+    And I should see category listing "Cat 1c" before "Cat 1a"
+    And I should see category listing "Cat 1a" before "Cat 2"
+    And I click to move category "CATB" up one
+    # Redirect. We should a 1, 1b, 1c, 1a, 2.
+    And I should see the "Course categories and courses" management page
+    And I should see category listing "Cat 1" before "Cat 1b"
+    And I should see category listing "Cat 1b" before "Cat 1c"
+    And I should see category listing "Cat 1c" before "Cat 1a"
+    And I should see category listing "Cat 1a" before "Cat 2"
+    And I click to move category "CAT2" up one
+    # Redirect. We should a 2, 1.
+    And I should see the "Course categories and courses" management page
+    And I should see category listing "Cat 2" before "Cat 1"
+    And I click on category "Cat 1" in the management interface
+    # Redirect. We should a 2, 1, 1b, 1c, 1a.
+    And I should see the "Course categories and courses" management page
+    And I should see category listing "Cat 2" before "Cat 1"
+    And I should see category listing "Cat 1" before "Cat 1b"
+    And I should see category listing "Cat 1b" before "Cat 1c"
+    And I should see category listing "Cat 1c" before "Cat 1a"
+
+  @javascript @_cross_browser
+  Scenario: Test using AJAX to move categories up and down by one.
+    Given the following "categories" exists:
+      | category | idnumber | name |
+      | 0 | CAT1 | Cat 1 |
+      | 0 | CAT2 | Cat 2 |
+      | CAT1 | CATA | Cat 1a |
+      | CAT1 | CATB | Cat 1b |
+      | CAT1 | CATC | Cat 1c |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I click on category "Cat 1" in the management interface
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I should see category listing "Cat 1" before "Cat 1a"
+    And I should see category listing "Cat 1a" before "Cat 1b"
+    And I should see category listing "Cat 1b" before "Cat 1c"
+    And I should see category listing "Cat 1c" before "Cat 2"
+    And I click to move category "CATA" down one
+    # AJAX request. No redirect.We should a 1, 1b, 1a, 1c, 2.
+    And I should see category listing "Cat 1" before "Cat 1b"
+    And I should see category listing "Cat 1b" before "Cat 1a"
+    And I should see category listing "Cat 1a" before "Cat 1c"
+    And I should see category listing "Cat 1c" before "Cat 2"
+    And I click to move category "CATC" up one
+    # AJAX request. No redirect. We should a 1, 1b, 1c, 1a, 2.
+    And I should see category listing "Cat 1" before "Cat 1b"
+    And I should see category listing "Cat 1b" before "Cat 1c"
+    And I should see category listing "Cat 1c" before "Cat 1a"
+    And I should see category listing "Cat 1a" before "Cat 2"
\ No newline at end of file
diff --git a/course/tests/behat/course_category_management_listing.feature b/course/tests/behat/course_category_management_listing.feature
new file mode 100644 (file)
index 0000000..4f75808
--- /dev/null
@@ -0,0 +1,660 @@
+@core @core_course
+Feature: Course category management interface performs as expected
+  In order to test JS enhanced display of categories and subcategories.
+  As a moodle admin
+  I need to expand and collapse categories.
+
+  @javascript
+  Scenario: Test general look of management interface
+    Given the following "categories" exists:
+      | name | category | idnumber |
+      | Cat 1 | 0 | CAT1 |
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see "Course and category management" in the "h2" "css_element"
+    And I should see "Viewing Course categories"
+    And I should see "Course categories" in the "h3" "css_element"
+    And I should see the "Course categories" management page
+
+  @javascript
+  Scenario: Test view mode functionality
+    Given the following "categories" exists:
+      | name | category | idnumber |
+      | Cat 1 | 0 | CAT1 |
+    And the following "courses" exists:
+      | fullname | shortname | category | format |
+      | Course 1 | C1 | CAT1 | topics |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I should see "Course categories" in the "#category-listing h3" "css_element"
+    And I should see "Cat 1" in the "#category-listing" "css_element"
+    And I should see "Viewing Course categories" in the ".view-mode-selector" "css_element"
+    And I should not see "Course categories and courses" in the ".view-mode-selector .menu" "css_element"
+    And I should not see "Course categories" in the ".view-mode-selector .menu" "css_element"
+    And I should not see "Courses" in the ".view-mode-selector .menu" "css_element"
+    When I click on "Viewing Course categories" "link" in the ".view-mode-selector" "css_element"
+    Then I should see "Course categories and courses" in the ".view-mode-selector .menu" "css_element"
+    And I should see "Course categories" in the ".view-mode-selector .menu" "css_element"
+    And I should see "Courses" in the ".view-mode-selector .menu" "css_element"
+    And I click on "Course categories and courses" "link" in the ".view-mode-selector .menu" "css_element"
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I should see "Course categories" in the "#category-listing h3" "css_element"
+    And I should see "Courses" in the "#course-listing h3" "css_element"
+    And I should see "Cat 1" in the "#category-listing" "css_element"
+    And I should see "Please select a category" in the "#course-listing" "css_element"
+    And I click on category "Cat 1" in the management interface
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I should see "Course categories" in the "#category-listing h3" "css_element"
+    And I should see "Cat 1" in the "#course-listing h3" "css_element"
+    And I should see "Cat 1" in the "#category-listing" "css_element"
+    And I should see "Course 1" in the "#course-listing" "css_element"
+    When I click on "Viewing Course categories" "link" in the ".view-mode-selector" "css_element"
+    Then I should see "Courses" in the ".view-mode-selector .menu" "css_element"
+    And I click on "Courses" "link" in the ".view-mode-selector .menu" "css_element"
+    # Redirect.
+    And I should see the "Courses" management page
+    And I should see "Cat 1" in the "#course-listing h3" "css_element"
+    And I should see "Course 1" in the "#course-listing" "css_element"
+    And I click on course "Course 1" in the management interface
+    # Redirect.
+    And I should see the "Courses" management page with a course selected
+    And I should see "Cat 1" in the "#course-listing h3" "css_element"
+    And I should see "Course 1" in the "#course-listing" "css_element"
+    And I should see "Course 1" in the "#course-detail h3" "css_element"
+    And I should see "C1" in the "#course-detail .shortname" "css_element"
+    And I should see "Course 1" in the "#course-detail .fullname" "css_element"
+    And I should see "Topics" in the "#course-detail .format" "css_element"
+    And I should see "Cat 1" in the "#course-detail .category" "css_element"
+
+  Scenario: Test displaying of sub categories
+    Given the following "categories" exists:
+      | name | category | idnumber |
+      | Cat 1 | 0 | CAT1 |
+      | Cat 2 | 0 | CAT2 |
+      | Cat 1-1 | CAT1 | CAT3 |
+      | Cat 1-2 | CAT1 | CAT4 |
+      | Cat 1-1-1 | CAT3 | CAT5 |
+      | Cat 1-1-2 | CAT3 | CAT6 |
+      | Cat 2-1 | CAT2 | CAT7 |
+      | Cat 2-1-1 | CAT7 | CAT8 |
+    And the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | C1 | CAT1 |
+      | Course 2 | C2 | CAT1 |
+      | Course 3 | C3 | CAT3 |
+      | Course 4 | C4 | CAT3 |
+      | Course 5 | C5 | CAT5 |
+      | Course 6 | C6 | CAT5 |
+      | Course 7 | C7 | CAT8 |
+      | Course 8 | C8 | CAT8 |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I should see "Cat 1"
+    And I should see "Cat 2"
+    And I should not see "Cat 1-1"
+    And I should not see "Cat 1-2"
+    And I should not see "Cat 1-1-1"
+    And I should not see "Cat 1-1-2"
+    And I should not see "Cat 2-1"
+    And I should not see "Cat 2-1-1"
+    And I click on "Cat 1" "link"
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I should see "Cat 1"
+    And I should see "Cat 2"
+    And I should see "Cat 1-1"
+    And I should see "Cat 1-2"
+    And I should not see "Cat 1-1-1"
+    And I should not see "Cat 1-1-2"
+    And I should not see "Cat 2-1"
+    And I should not see "Cat 2-1-1"
+    And I click on "Cat 1-1" "link"
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I should see "Cat 1"
+    And I should see "Cat 2"
+    And I should see "Cat 1-1"
+    And I should see "Cat 1-2"
+    And I should see "Cat 1-1-1"
+    And I should see "Cat 1-1-2"
+    And I should not see "Cat 2-1"
+    And I should not see "Cat 2-1-1"
+    And I click on "Cat 2" "link"
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I should see "Cat 1"
+    And I should see "Cat 2"
+    And I should not see "Cat 1-1"
+    And I should not see "Cat 1-2"
+    And I should not see "Cat 1-1-1"
+    And I should not see "Cat 1-1-2"
+    And I should see "Cat 2-1"
+    And I should not see "Cat 2-1-1"
+
+  # This is similar to the above scenario except here we are going to use AJAX
+  # to load the categories.
+  @javascript @_cross_browser
+  Scenario: Test AJAX loading of sub categories
+    Given the following "categories" exists:
+      | name | category | idnumber |
+      | Cat 1 | 0 | CAT1 |
+      | Cat 2 | 0 | CAT2 |
+      | Cat 1-1 | CAT1 | CAT3 |
+      | Cat 1-2 | CAT1 | CAT4 |
+      | Cat 1-1-1 | CAT3 | CAT5 |
+      | Cat 1-1-2 | CAT3 | CAT6 |
+      | Cat 2-1 | CAT2 | CAT7 |
+      | Cat 2-1-1 | CAT7 | CAT8 |
+    And the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | C1 | CAT1 |
+      | Course 2 | C2 | CAT1 |
+      | Course 3 | C3 | CAT3 |
+      | Course 4 | C4 | CAT3 |
+      | Course 5 | C5 | CAT5 |
+      | Course 6 | C6 | CAT5 |
+      | Course 7 | C7 | CAT8 |
+      | Course 8 | C8 | CAT8 |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I should see "Cat 1"
+    And I should see "Cat 2"
+    And I should not see "Cat 1-1"
+    And I should not see "Cat 1-2"
+    And I should not see "Cat 1-1-1"
+    And I should not see "Cat 1-1-2"
+    And I should not see "Cat 2-1"
+    And I should not see "Cat 2-1-1"
+    And I click to expand category "CAT1" in the management interface
+    # AJAX action - no redirect.
+    And I should see "Cat 1"
+    And I should see "Cat 2"
+    And I should see "Cat 1-1"
+    And I should see "Cat 1-2"
+    And I should not see "Cat 1-1-1"
+    And I should not see "Cat 1-1-2"
+    And I should not see "Cat 2-1"
+    And I should not see "Cat 2-1-1"
+    And I click to expand category "CAT3" in the management interface
+    # AJAX action - no redirect.
+    And I should see "Cat 1"
+    And I should see "Cat 2"
+    And I should see "Cat 1-1"
+    And I should see "Cat 1-2"
+    And I should see "Cat 1-1-1"
+    And I should see "Cat 1-1-2"
+    And I should not see "Cat 2-1"
+    And I should not see "Cat 2-1-1"
+    And I click to expand category "CAT2" in the management interface
+    # AJAX action - no redirect.
+    And I should see "Cat 1"
+    And I should see "Cat 2"
+    And I should see "Cat 1-1"
+    And I should see "Cat 1-2"
+    And I should see "Cat 1-1-1"
+    And I should see "Cat 1-1-2"
+    And I should see "Cat 2-1"
+    And I should not see "Cat 2-1-1"
+    And I click to expand category "CAT7" in the management interface
+    # AJAX action - no redirect.
+    And I should see "Cat 1"
+    And I should see "Cat 2"
+    And I should see "Cat 1-1"
+    And I should see "Cat 1-2"
+    And I should see "Cat 1-1-1"
+    And I should see "Cat 1-1-2"
+    And I should see "Cat 2-1"
+    And I should see "Cat 2-1-1"
+    And I click to expand category "CAT1" in the management interface
+    # AJAX action - no redirect.
+    And I should see "Cat 1"
+    And I should see "Cat 2"
+    And I should not see "Cat 1-1"
+    And I should not see "Cat 1-2"
+    And I should not see "Cat 1-1-1"
+    And I should not see "Cat 1-1-2"
+    And I should see "Cat 2-1"
+    And I should see "Cat 2-1-1"
+    And I click to expand category "CAT1" in the management interface
+    # AJAX action - no redirect.
+    And I should see "Cat 1"
+    And I should see "Cat 2"
+    And I should see "Cat 1-1"
+    And I should see "Cat 1-2"
+    And I should see "Cat 1-1-1"
+    And I should see "Cat 1-1-2"
+    And I should see "Cat 2-1"
+    And I should see "Cat 2-1-1"
+
+  @javascript
+  Scenario Outline: Top level categories are displayed correctly when resorted
+    Given the following "categories" exists:
+      | category | name | idnumber | sortorder |
+      | 0 | Social studies | Ext003 | 1 |
+      | 0 | Applied sciences | Sci001 | 2 |
+      | 0 | Extended social studies | Ext002 | 3 |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I click on "Re-sort categories" "link"
+    And I should see "By name" in the ".category-listing-actions" "css_element"
+    And I should see "By idnumber" in the ".category-listing-actions" "css_element"
+    And I click on <sortby> "link" in the ".category-listing-actions" "css_element"
+    # Redirect.
+    And I should see the "Course categories" management page
+    And I should see category listing <cat1> before <cat2>
+    And I should see category listing <cat2> before <cat3>
+
+  Examples:
+    | sortby | cat1 | cat2 | cat3 |
+    | "Re-sort categories" | "Social studies"          | "Applied sciences"        | "Extended social studies" |
+    | "By name"            | "Applied sciences"        | "Extended social studies" | "Social studies" |
+    | "By idnumber"        | "Extended social studies" | "Social studies" | "Applied sciences" |
+
+  @javascript
+  Scenario Outline: Sub categories are displayed correctly when resorted
+    Given the following "categories" exists:
+      | category | name | idnumber | sortorder |
+      | 0 | Master cat  | CAT1 | 1 |
+      | CAT1 | Social studies | Ext003 | 1 |
+      | CAT1 | Applied sciences | Sci001 | 2 |
+      | CAT1 | Extended social studies | Ext002 | 3 |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I click on "Master cat" "link"
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I click on "Re-sort subcategories" "link"
+    And I should see "By name" in the ".category-listing-actions" "css_element"
+    And I should see "By idnumber" in the ".category-listing-actions" "css_element"
+    And I click on <sortby> "link" in the ".category-listing-actions" "css_element"
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I should see category listing <cat1> before <cat2>
+    And I should see category listing <cat2> before <cat3>
+
+  Examples:
+    | sortby | cat1 | cat2 | cat3 |
+    | "Re-sort subcategories" | "Social studies"          | "Applied sciences"        | "Extended social studies" |
+    | "By name"            | "Applied sciences"        | "Extended social studies" | "Social studies" |
+    | "By idnumber"        | "Extended social studies" | "Social studies" | "Applied sciences" |
+
+  @javascript
+  Scenario Outline: Test courses are displayed correctly after being resorted.
+    Given the following "categories" exists:
+      | name | category 0| idnumber |
+      | Cat 1 | 0 | CAT1 |
+    And the following "courses" exists:
+      | category | fullname | shortname | idnumber | sortorder |
+      | CAT1 | Social studies | Senior school | Ext003 | 1 |
+      | CAT1 | Applied sciences  | Middle school | Sci001 | 2 |
+      | CAT1 | Extended social studies  | Junior school | Ext002 | 3 |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I click on "Cat 1" "link"
+  # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I click on "Re-sort courses" "link"
+    And I should see "By fullname" in the ".course-listing-actions" "css_element"
+    And I should see "By shortname" in the ".course-listing-actions" "css_element"
+    And I should see "By idnumber" in the ".course-listing-actions" "css_element"
+    And I click on <sortby> "link" in the ".course-listing-actions" "css_element"
+  # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I should see course listing <course1> before <course2>
+    And I should see course listing <course2> before <course3>
+
+  Examples:
+    | sortby | course1 | course2 | course3 |
+    | "By fullname"        | "Applied sciences"        | "Extended social studies" | "Social studies" |
+    | "By shortname"       | "Extended social studies" | "Applied sciences"        | "Social studies" |
+    | "By idnumber"        | "Extended social studies" | "Social studies"          | "Applied sciences" |
+
+  @javascript
+  Scenario: Test course pagination
+    Given the following "categories" exists:
+      | name | category | idnumber |
+      | Cat 1 | 0 | CAT1 |
+    And the following "courses" exists:
+      | category | fullname | shortname | idnumber |
+      | CAT1 | Course 1 | Course 1 | C1 |
+      | CAT1 | Course 2 | Course 2 | C2 |
+      | CAT1 | Course 3 | Course 3 | C3 |
+      | CAT1 | Course 4 | Course 4 | C4 |
+      | CAT1 | Course 5 | Course 5 | C5 |
+      | CAT1 | Course 6 | Course 6 | C6 |
+      | CAT1 | Course 7 | Course 7 | C7 |
+      | CAT1 | Course 8 | Course 8 | C8 |
+      | CAT1 | Course 9 | Course 9 | C9 |
+      | CAT1 | Course 10 | Course 10 | C10 |
+      | CAT1 | Course 11 | Course 11 | C11 |
+      | CAT1 | Course 12 | Course 12 | C12 |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I click on "Cat 1" "link"
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I click on "Re-sort courses" "link"
+    And I click on "By idnumber" "link" in the ".course-listing-actions" "css_element"
+    # Redirect.
+    And I should see "Per page: 20" in the ".course-listing-actions" "css_element"
+    And I should see course listing "Course 1" before "Course 2"
+    And I should see course listing "Course 2" before "Course 3"
+    And I should see course listing "Course 3" before "Course 4"
+    And I should see course listing "Course 4" before "Course 5"
+    And I should see course listing "Course 5" before "Course 6"
+    And I should see course listing "Course 6" before "Course 7"
+    And I should see course listing "Course 7" before "Course 8"
+    And I should see course listing "Course 8" before "Course 9"
+    And I should see course listing "Course 9" before "Course 10"
+    And I should see course listing "Course 10" before "Course 11"
+    And I should see course listing "Course 11" before "Course 12"
+    And "#course-listing .listing-pagination" "css_element" should not exists
+    And I click on "Per page: 20" "link" in the ".course-listing-actions" "css_element"
+    And I should see "5" in the ".courses-per-page" "css_element"
+    And I should see "10" in the ".courses-per-page" "css_element"
+    And I should see "20" in the ".courses-per-page" "css_element"
+    And I should see "All" in the ".courses-per-page" "css_element"
+    And I click on "5" "link" in the ".courses-per-page" "css_element"
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I should see "Per page: 5" in the ".course-listing-actions" "css_element"
+    And I should see course listing "Course 1" before "Course 2"
+    And I should see course listing "Course 2" before "Course 3"
+    And I should see course listing "Course 3" before "Course 4"
+    And I should see course listing "Course 4" before "Course 5"
+    And I should not see "Course 6"
+    And I should not see "Course 7"
+    And I should not see "Course 8"
+    And I should not see "Course 9"
+    And I should not see "Course 10"
+    And I should not see "Course 11"
+    And I should not see "Course 12"
+    And "#course-listing .listing-pagination" "css_element" should exists
+    And I should see "Showing courses 1 to 5 of 12 courses"
+    And I should not see "First" in the "#course-listing .listing-pagination" "css_element"
+    And I should not see "Prev" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "1" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "2" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "3" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "Next" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "Last" in the "#course-listing .listing-pagination" "css_element"
+    And I click on "2" "link" in the "#course-listing .listing-pagination" "css_element"
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I should see "Per page: 5" in the ".course-listing-actions" "css_element"
+    And I should not see "Course 1"
+    And I should not see "Course 2"
+    And I should not see "Course 3"
+    And I should not see "Course 4"
+    And I should not see "Course 5"
+    And I should see course listing "Course 6" before "Course 7"
+    And I should see course listing "Course 7" before "Course 8"
+    And I should see course listing "Course 8" before "Course 9"
+    And I should see course listing "Course 9" before "Course 10"
+    And I should not see "Course 11"
+    And I should not see "Course 12"
+    And "#course-listing .listing-pagination" "css_element" should exists
+    And I should see "Showing courses 6 to 10 of 12 courses"
+    And I should see "First" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "Prev" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "1" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "2" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "3" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "Next" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "Last" in the "#course-listing .listing-pagination" "css_element"
+    And I click on "Next" "link" in the "#course-listing .listing-pagination" "css_element"
+    # Redirect. Test next link.
+    And I should see the "Course categories and courses" management page
+    And I should see "Per page: 5" in the ".course-listing-actions" "css_element"
+    And I should not see "Course 1"
+    And I should not see "Course 2"
+    And I should not see "Course 3"
+    And I should not see "Course 4"
+    And I should not see "Course 5"
+    And I should not see "Course 6"
+    And I should not see "Course 7"
+    And I should not see "Course 8"
+    And I should not see "Course 9"
+    And I should not see "Course 10"
+    And I should see course listing "Course 11" before "Course 12"
+    And "#course-listing .listing-pagination" "css_element" should exists
+    And I should see "Showing courses 11 to 12 of 12 courses"
+    And I should see "First" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "Prev" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "1" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "2" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "3" in the "#course-listing .listing-pagination" "css_element"
+    And I should not see "Next" in the "#course-listing .listing-pagination" "css_element"
+    And I should not see "Last" in the "#course-listing .listing-pagination" "css_element"
+    And I click on "First" "link" in the "#course-listing .listing-pagination" "css_element"
+    # Redirect. Test first link.
+    And I should see the "Course categories and courses" management page
+    And I should see "Per page: 5" in the ".course-listing-actions" "css_element"
+    And I should see course listing "Course 1" before "Course 2"
+    And I should see course listing "Course 2" before "Course 3"
+    And I should see course listing "Course 3" before "Course 4"
+    And I should see course listing "Course 4" before "Course 5"
+    And I should not see "Course 6"
+    And I should not see "Course 7"
+    And I should not see "Course 8"
+    And I should not see "Course 9"
+    And I should not see "Course 10"
+    And I should not see "Course 11"
+    And I should not see "Course 12"
+    And "#course-listing .listing-pagination" "css_element" should exists
+    And I should see "Showing courses 1 to 5 of 12 courses"
+    And I should not see "First" in the "#course-listing .listing-pagination" "css_element"
+    And I should not see "Prev" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "1" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "2" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "3" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "Next" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "Last" in the "#course-listing .listing-pagination" "css_element"
+    And I click on "Last" "link" in the "#course-listing .listing-pagination" "css_element"
+    # Redirect. Test last link.
+    And I should see the "Course categories and courses" management page
+    And I should see "Per page: 5" in the ".course-listing-actions" "css_element"
+    And I should not see "Course 1"
+    And I should not see "Course 2"
+    And I should not see "Course 3"
+    And I should not see "Course 4"
+    And I should not see "Course 5"
+    And I should not see "Course 6"
+    And I should not see "Course 7"
+    And I should not see "Course 8"
+    And I should not see "Course 9"
+    And I should not see "Course 10"
+    And I should see course listing "Course 11" before "Course 12"
+    And "#course-listing .listing-pagination" "css_element" should exists
+    And I should see "Showing courses 11 to 12 of 12 courses"
+    And I should see "First" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "Prev" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "1" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "2" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "3" in the "#course-listing .listing-pagination" "css_element"
+    And I should not see "Next" in the "#course-listing .listing-pagination" "css_element"
+    And I should not see "Last" in the "#course-listing .listing-pagination" "css_element"
+    And I click on "Prev" "link" in the "#course-listing .listing-pagination" "css_element"
+    # Redirect. Test prev link.
+    And I should see the "Course categories and courses" management page
+    And I should see "Per page: 5" in the ".course-listing-actions" "css_element"
+    And I should not see "Course 1"
+    And I should not see "Course 2"
+    And I should not see "Course 3"
+    And I should not see "Course 4"
+    And I should not see "Course 5"
+    And I should see course listing "Course 6" before "Course 7"
+    And I should see course listing "Course 7" before "Course 8"
+    And I should see course listing "Course 8" before "Course 9"
+    And I should see course listing "Course 9" before "Course 10"
+    And I should not see "Course 11"
+    And I should not see "Course 12"
+    And "#course-listing .listing-pagination" "css_element" should exists
+    And I should see "Showing courses 6 to 10 of 12 courses"
+    And I should see "First" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "Prev" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "1" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "2" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "3" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "Next" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "Last" in the "#course-listing .listing-pagination" "css_element"
+
+  # We need at least 30 courses for this next test.
+  @javascript
+  Scenario: Test many course pagination
+    Given the following "categories" exists:
+      | name | category 0| idnumber |
+      | Cat 1 | 0 | CAT1 |
+    And the following "courses" exists:
+      | category | fullname | shortname | idnumber |
+      | CAT1 | Course 1 | Course 1 | C1 |
+      | CAT1 | Course 2 | Course 2 | C2 |
+      | CAT1 | Course 3 | Course 3 | C3 |
+      | CAT1 | Course 4 | Course 4 | C4 |
+      | CAT1 | Course 5 | Course 5 | C5 |
+      | CAT1 | Course 6 | Course 6 | C6 |
+      | CAT1 | Course 7 | Course 7 | C7 |
+      | CAT1 | Course 8 | Course 8 | C8 |
+      | CAT1 | Course 9 | Course 9 | C9 |
+      | CAT1 | Course 10 | Course 10 | C10 |
+      | CAT1 | Course 11 | Course 11 | C11 |
+      | CAT1 | Course 12 | Course 12 | C12 |
+      | CAT1 | Course 13 | Course 13 | C13 |
+      | CAT1 | Course 14 | Course 14 | C14 |
+      | CAT1 | Course 15 | Course 15 | C15 |
+      | CAT1 | Course 16 | Course 16 | C16 |
+      | CAT1 | Course 17 | Course 17 | C17 |
+      | CAT1 | Course 18 | Course 18 | C18 |
+      | CAT1 | Course 19 | Course 19 | C19 |
+      | CAT1 | Course 20 | Course 20 | C20 |
+      | CAT1 | Course 21 | Course 21 | C21 |
+      | CAT1 | Course 22 | Course 22 | C22 |
+      | CAT1 | Course 23 | Course 23 | C23 |
+      | CAT1 | Course 24 | Course 24 | C24 |
+      | CAT1 | Course 25 | Course 25 | C25 |
+      | CAT1 | Course 26 | Course 26 | C26 |
+      | CAT1 | Course 27 | Course 27 | C27 |
+      | CAT1 | Course 28 | Course 28 | C28 |
+      | CAT1 | Course 29 | Course 29 | C29 |
+      | CAT1 | Course 30 | Course 30 | C30 |
+      | CAT1 | Course 31 | Course 31 | C31 |
+      | CAT1 | Course 32 | Course 32 | C32 |
+
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see the "Course categories" management page
+    And I click on "Cat 1" "link"
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I click on "Re-sort courses" "link"
+    And I click on "By idnumber" "link" in the ".course-listing-actions" "css_element"
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I should see "Per page: 20" in the ".course-listing-actions" "css_element"
+    And I should see course listing "Course 1" before "Course 2"
+    And I should see course listing "Course 19" before "Course 20"
+    And I should not see "Course 21"
+    And I should see "Showing courses 1 to 20 of 32 courses"
+    And I click on "Per page: 20" "link" in the ".course-listing-actions" "css_element"
+    And I click on "100" "link" in the ".courses-per-page" "css_element"
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I should see "Per page: 100" in the ".course-listing-actions" "css_element"
+    And I should see course listing "Course 1" before "Course 2"
+    And I should see course listing "Course 19" before "Course 20"
+    And I should see course listing "Course 21" before "Course 22"
+    And I should see course listing "Course 31" before "Course 32"
+    And "#course-listing .listing-pagination" "css_element" should not exists
+    And I click on "Per page: 100" "link" in the ".course-listing-actions" "css_element"
+    And I click on "5" "link" in the ".courses-per-page" "css_element"
+    # Redirect.
+    And I should see the "Course categories and courses" management page
+    And I should see "Per page: 5" in the ".course-listing-actions" "css_element"
+    And I should see course listing "Course 1" before "Course 2"
+    And I should see course listing "Course 4" before "Course 5"
+    And I should not see "Course 6"
+    And I should see "Showing courses 1 to 5 of 32 courses"
+    And I should not see "First" in the "#course-listing .listing-pagination" "css_element"
+    And I should not see "Prev" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "1" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "2" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "3" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "4" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "5" in the "#course-listing .listing-pagination" "css_element"
+    And I should not see "6" in the "#course-listing .listing-pagination" "css_element"
+    And I should not see "7" in the "#course-listing .listing-pagination" "css_element"
+    And I should see "Next" in the "#course-listing .listing-pagination" "css_element"
+    And I&n