Merge branch 'wip-MDL-40634-m26' of git://github.com/samhemelryk/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 16 Jul 2013 22:58:26 +0000 (00:58 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 16 Jul 2013 22:58:26 +0000 (00:58 +0200)
164 files changed:
admin/cli/alternative_component_cache.php [new file with mode: 0644]
admin/cli/install.php
admin/index.php
admin/oacleanup.php [deleted file]
admin/roles/assign.php
admin/roles/classes/capability_table_base.php
admin/settings/plugins.php
admin/tool/behat/cli/init.php
admin/tool/behat/cli/util.php
admin/tool/behat/lang/en/tool_behat.php
admin/tool/behat/renderer.php
admin/tool/phpunit/cli/init.php
admin/tool/phpunit/cli/util.php
admin/tool/uploadcourse/classes/base_form.php [new file with mode: 0644]
admin/tool/uploadcourse/classes/course.php [new file with mode: 0644]
admin/tool/uploadcourse/classes/helper.php [new file with mode: 0644]
admin/tool/uploadcourse/classes/processor.php [new file with mode: 0644]
admin/tool/uploadcourse/classes/step1_form.php [new file with mode: 0644]
admin/tool/uploadcourse/classes/step2_form.php [new file with mode: 0644]
admin/tool/uploadcourse/classes/tracker.php [new file with mode: 0644]
admin/tool/uploadcourse/cli/uploadcourse.php [new file with mode: 0644]
admin/tool/uploadcourse/db/caches.php [moved from enrol/authorize/db/messages.php with 79% similarity]
admin/tool/uploadcourse/index.php [new file with mode: 0644]
admin/tool/uploadcourse/lang/en/tool_uploadcourse.php [new file with mode: 0644]
admin/tool/uploadcourse/settings.php [moved from enrol/authorize/db/upgrade.php with 59% similarity]
admin/tool/uploadcourse/tests/behat/create.feature [new file with mode: 0644]
admin/tool/uploadcourse/tests/behat/update.feature [new file with mode: 0644]
admin/tool/uploadcourse/tests/course_test.php [new file with mode: 0644]
admin/tool/uploadcourse/tests/fixtures/backup.mbz [new file with mode: 0644]
admin/tool/uploadcourse/tests/fixtures/courses.csv [new file with mode: 0644]
admin/tool/uploadcourse/tests/helper_test.php [new file with mode: 0644]
admin/tool/uploadcourse/tests/processor_test.php [new file with mode: 0644]
admin/tool/uploadcourse/version.php [moved from enrol/authorize/version.php with 65% similarity]
admin/tool/xmldb/actions/create_xml_file/create_xml_file.class.php
admin/webservice/forms.php
backup/moodle2/restore_stepslib.php
backup/util/dbops/restore_dbops.class.php
badges/newbadge.php
badges/tests/badgeslib_test.php
blocks/dock.js [deleted file]
blocks/moodleblock.class.php
blocks/navigation/block_navigation.php
blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js
blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js
blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js
blocks/navigation/yui/src/navigation/js/navigation.js
blocks/navigation/yui/src/navigation/meta/navigation.json
blocks/section_links/block_section_links.php
blocks/settings/block_settings.php
cache/classes/loaders.php
cache/stores/session/lib.php
cache/stores/static/lib.php
cache/tests/cache_test.php
cache/tests/fixtures/lib.php
calendar/export.php
course/externallib.php
course/lib.php
course/modlib.php
course/moodleform_mod.php
course/tests/externallib_test.php
enrol/authorize/authorize.js [deleted file]
enrol/authorize/authorizenet.class.php [deleted file]
enrol/authorize/const.php [deleted file]
enrol/authorize/db/access.php [deleted file]
enrol/authorize/db/install.xml [deleted file]
enrol/authorize/enrol_form.php [deleted file]
enrol/authorize/index.php [deleted file]
enrol/authorize/lang/en/enrol_authorize.php [deleted file]
enrol/authorize/localfuncs.php [deleted file]
enrol/authorize/locallib.php [deleted file]
enrol/authorize/uploadcsv.php [deleted file]
filter/activitynames/filter.php
filter/data/filter.php
filter/glossary/filter.php
filter/manage.php
filter/urltolink/filter.php
filter/urltolink/tests/filter_test.php
grade/report/grader/lib.php
install.php
lib/accesslib.php
lib/adminlib.php
lib/badgeslib.php
lib/behat/classes/behat_command.php
lib/blocklib.php
lib/classes/component.php
lib/coursecatlib.php
lib/cronlib.php
lib/csslib.php
lib/db/caches.php
lib/db/install.xml
lib/db/upgrade.php
lib/deprecatedlib.php
lib/editor/tinymce/classes/plugin.php
lib/editor/tinymce/subplugins.php
lib/filebrowser/file_info_context_module.php
lib/javascript.php
lib/moodlelib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/pagelib.php
lib/phpunit/bootstrap.php
lib/pluginlib.php
lib/setup.php
lib/setuplib.php
lib/tests/accesslib_test.php
lib/tests/behat/behat_general.php
lib/tests/component_test.php
lib/tests/weblib_test.php
lib/upgrade.txt
lib/upgradelib.php
lib/weblib.php
lib/yui/build/moodle-core-dock/moodle-core-dock-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-dock/moodle-core-dock-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-dock/moodle-core-dock.js [new file with mode: 0644]
lib/yui/build/moodle-core-dockloader/moodle-core-dockloader-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-dockloader/moodle-core-dockloader-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-dockloader/moodle-core-dockloader.js [new file with mode: 0644]
lib/yui/src/dock/build.json [new file with mode: 0644]
lib/yui/src/dock/js/actionkey.js [new file with mode: 0644]
lib/yui/src/dock/js/block.js [new file with mode: 0644]
lib/yui/src/dock/js/dock.js [new file with mode: 0644]
lib/yui/src/dock/js/dockeditem.js [new file with mode: 0644]
lib/yui/src/dock/js/dockloader.js [new file with mode: 0644]
lib/yui/src/dock/js/panel.js [new file with mode: 0644]
lib/yui/src/dock/js/tabheightmanager.js [new file with mode: 0644]
lib/yui/src/dock/meta/dock.json [new file with mode: 0644]
mod/assign/locallib.php
mod/lesson/format.php
mod/lesson/locallib.php
mod/scorm/report/objectives/lang/en/scormreport_objectives.php [new file with mode: 0644]
mod/scorm/report/objectives/report.php [new file with mode: 0644]
mod/scorm/report/objectives/responsessettings_form.php [new file with mode: 0644]
mod/scorm/report/objectives/version.php [new file with mode: 0644]
mod/wiki/locallib.php
mod/workshop/allocation/manual/lib.php
repository/coursefiles/lib.php
repository/skydrive/db/access.php [new file with mode: 0644]
repository/skydrive/db/caches.php [moved from enrol/authorize/import_form.php with 57% similarity]
repository/skydrive/lang/en/repository_skydrive.php [new file with mode: 0644]
repository/skydrive/lib.php [new file with mode: 0644]
repository/skydrive/microsoftliveapi.php [new file with mode: 0644]
repository/skydrive/pix/icon.png [new file with mode: 0644]
repository/skydrive/version.php [new file with mode: 0644]
theme/bootstrapbase/layout/columns1.php
theme/bootstrapbase/layout/columns2.php
theme/bootstrapbase/layout/columns3.php
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/less/moodle/responsive.less
theme/bootstrapbase/style/moodle.css
theme/clean/layout/columns1.php
theme/clean/layout/columns2.php
theme/clean/layout/columns3.php
theme/image.php
theme/javascript.php
theme/jquery.php
theme/styles.php
theme/styles_debug.php
theme/yui_combo.php
theme/yui_image.php
version.php
webservice/externallib.php
webservice/tests/externallib_test.php

diff --git a/admin/cli/alternative_component_cache.php b/admin/cli/alternative_component_cache.php
new file mode 100644 (file)
index 0000000..6b3d454
--- /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/>.
+
+/**
+ * This hack is intended for clustered sites that do not want
+ * to use shared cachedir for component cache.
+ *
+ * This file needs to be called after any change in PHP files in dataroot,
+ * that is before upgrade and install.
+ *
+ * @package   core
+ * @copyright 2013 Petr Skoda (skodak)  {@link http://skodak.org}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+define('CLI_SCRIPT', true);
+define('ABORT_AFTER_CONFIG', true); // We need just the values from config.php.
+define('CACHE_DISABLE_ALL', true); // This prevents reading of existing caches.
+define('IGNORE_COMPONENT_CACHE', true);
+
+require(__DIR__.'/../../config.php');
+require_once($CFG->libdir.'/clilib.php');
+
+// Now get cli options.
+list($options, $unrecognized) = cli_get_params(
+    array(
+        'file'    => false,
+        'rebuild' => false,
+        'print'   => false,
+        'help'    => false
+    ),
+    array(
+        'h' => 'help'
+    )
+);
+
+if ($unrecognized) {
+    $unrecognized = implode("\n  ", $unrecognized);
+    cli_error(get_string('cliunknowoption', 'admin', $unrecognized), 2);
+}
+
+if (!$options['rebuild'] and !$options['file'] and !$options['print']) {
+    $help =
+"Create alternative component cache file
+
+Options:
+-h, --help            Print out this help
+--rebuild             Rebuild \$CFG->alternative_component_cache file
+--file=filepath       Save component cache to file
+--print               Print component cache file content
+
+Example:
+\$ php admin/cli/rebuild_alternative_component_cache.php --rebuild
+";
+
+    echo $help;
+    exit(0);
+}
+
+error_reporting(E_ALL | E_STRICT);
+ini_set('display_errors', 1);
+
+$content = core_component::get_cache_content();
+
+if ($options['print']) {
+    echo $content;
+    exit(0);
+}
+
+if ($options['rebuild']) {
+    if (empty($CFG->alternative_component_cache)) {
+        fwrite(STDERR, 'config.php does not contain $CFG->alternative_component_cache setting');
+        fwrite(STDERR, "\n");
+        exit(2);
+    }
+    $target = $CFG->alternative_component_cache;
+} else {
+    $target = $options['file'];
+}
+
+if (!$target) {
+    fwrite(STDERR, "Invalid target file $target");
+    fwrite(STDERR, "\n");
+    exit(1);
+}
+
+$bytes = file_put_contents($target, $content);
+
+if (!$bytes) {
+    fwrite(STDERR, "Error writing to $target");
+    fwrite(STDERR, "\n");
+    exit(1);
+}
+
+// Success.
+echo "File $target was updated\n";
+exit(0);
index f0bee31..c2d08d1 100644 (file)
@@ -139,6 +139,8 @@ define('CACHE_DISABLE_ALL', true);
 
 define('PHPUNIT_TEST', false);
 
+define('IGNORE_COMPONENT_CACHE', true);
+
 // Check that PHP is of a sufficient version
 if (version_compare(phpversion(), "5.3.3") < 0) {
     $phpversion = phpversion();
index 9acf551..1e9e810 100644 (file)
@@ -47,10 +47,11 @@ if (!function_exists('iconv')) {
 
 define('NO_OUTPUT_BUFFERING', true);
 
-if (empty($_GET['cache']) and empty($_POST['cache'])) {
+if (empty($_GET['cache']) and empty($_POST['cache']) and empty($_GET['sesskey']) and empty($_POST['sesskey'])) {
     // Prevent caching at all cost when visiting this page directly,
     // we redirect to self once we known no upgrades are necessary.
     // Note: $_GET and $_POST are used here intentionally because our param cleaning is not loaded yet.
+    // Note2: the sesskey is present in all block editing hacks, we can not redirect there, so enable caching.
     define('CACHE_DISABLE_ALL', true);
 
     // Force OPcache reset if used, we do not want any stale caches
@@ -449,7 +450,7 @@ if (during_initial_install()) {
 
 // Now we can be sure everything was upgraded and caches work fine,
 // redirect if necessary to make sure caching is enabled.
-if (!$cache) {
+if (!$cache and !optional_param('sesskey', '', PARAM_RAW)) {
     redirect(new moodle_url($PAGE->url, array('cache' => 1)));
 }
 
diff --git a/admin/oacleanup.php b/admin/oacleanup.php
deleted file mode 100644 (file)
index faf55f5..0000000
+++ /dev/null
@@ -1,174 +0,0 @@
-<?php
-
-if (!isset($CFG)) {
-
-    require('../config.php');
-    require_once($CFG->libdir.'/adminlib.php');
-
-    admin_externalpage_setup('oacleanup');
-
-    echo $OUTPUT->header();
-    online_assignment_cleanup(true);
-    echo $OUTPUT->footer();
-
-}
-
-
-
-function online_assignment_cleanup($output=false) {
-    global $CFG, $DB, $OUTPUT;
-
-    if ($output) {
-        echo $OUTPUT->heading('Online Assignment Cleanup');
-        echo '<center>';
-    }
-
-
-    /// We don't want to run this code if we are doing an upgrade from an assignment
-    /// version earlier than 2005041400
-    /// because the assignment type field will not exist
-    $amv = $DB->get_field('modules', 'version', array('name'=>'assignment'));
-    if ((int)$amv < 2005041400) {
-        if ($output) {
-            echo '</center>';
-        }
-        return;
-    }
-
-
-    /// get the module id for assignments from db
-    $arecord = $DB->get_record('modules', array('name', 'assignment'));
-    $aid = $arecord->id;
-
-
-    /// get a list of all courses on this site
-    list($ctxselect, $ctxjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
-    $sql = "SELECT c.* $ctxselect FROM {course} c $ctxjoin";
-    $courses = $DB->get_records_sql($sql);
-
-    /// cycle through each course
-    foreach ($courses as $course) {
-        context_helper::preload_from_record($course);
-        $context = context_course::instance($course->id);
-
-        if (empty($course->fullname)) {
-            $fullname = get_string('course').': '.$course->id;
-        } else {
-            $fullname = format_string($course->fullname, true, array('context' => $context));
-        }
-        if ($output) echo $OUTPUT->heading($fullname);
-
-        /// retrieve a list of sections beyond what is currently being shown
-        $courseformatoptions = course_get_format($course)->get_format_options();
-        if (!isset($courseformatoptions['numsections'])) {
-            // Course format does not use numsections
-            if ($output) {
-                echo 'No extra sections<br />';
-            }
-            continue;
-        }
-        $sql = "SELECT *
-                  FROM {course_sections}
-                 WHERE course=? AND section>?
-              ORDER BY section ASC";
-        $params = array($course->id, $courseformatoptions['numsections']);
-        if (!($xsections = $DB->get_records_sql($sql, $params))) {
-            if ($output) echo 'No extra sections<br />';
-            continue;
-        }
-
-        /// cycle through each of the xtra sections
-        foreach ($xsections as $xsection) {
-
-            if ($output) echo 'Checking Section: '.$xsection->section.'<br />';
-
-            /// grab any module instances from the sequence field
-            if (!empty($xsection->sequence)) {
-                $instances = explode(',', $xsection->sequence);
-
-                /// cycle through the instances
-                foreach ($instances as $instance) {
-                    /// is this an instance of an online assignment
-                    $sql = "SELECT a.id
-                              FROM  {course_modules} cm, {assignment} a
-                             WHERE cm.id = ? AND cm.module = ? AND
-                                   cm.instance = a.id AND a.assignmenttype = 'online'";
-                    $params = array($instance, $aid);
-
-                    /// if record exists then we need to move instance to it's correct section
-                    if ($DB->record_exists_sql($sql, $params)) {
-
-                        /// check the new section id
-                        /// the journal update erroneously stored it in course_sections->section
-                        $newsection = $xsection->section;
-                        /// double check the new section
-                        if ($newsection > $courseformatoptions['numsections']) {
-                            /// get the record for section 0 for this course
-                            if (!($zerosection = $DB->get_record('course_sections', array('course'=>$course->id, 'section'=>'0')))) {
-                                continue;
-                            }
-                            $newsection = $zerosection->id;
-                        }
-
-                        /// grab the section record
-                        if (!($section = $DB->get_record('course_sections', array('id'=>$newsection)))) {
-                            if ($output) {
-                                echo 'Serious error: Cannot retrieve section: '.$newsection.' for course: '. $fullname .'<br />';
-                            }
-                            continue;
-                        }
-
-                        /// explode the sequence
-                        if  (($sequence = explode(',', $section->sequence)) === false) {
-                            $sequence = array();
-                        }
-
-                        /// add instance to correct section
-                        array_push($sequence, $instance);
-
-                        /// implode the sequence
-                        $section->sequence = implode(',', $sequence);
-
-                        $DB->set_field('course_sections', 'sequence', $section->sequence, array('id'=>$section->id));
-
-                        /// now we need to remove the instance from the old sequence
-
-                        /// grab the old section record
-                        if (!($section = $DB->get_record('course_sections', array('id'=>$xsection->id)))) {
-                            if ($output) echo 'Serious error: Cannot retrieve old section: '.$xsection->id.' for course: '.$fullname.'<br />';
-                            continue;
-                        }
-
-                        /// explode the sequence
-                        if  (($sequence = explode(',', $section->sequence)) === false) {
-                            $sequence = array();
-                        }
-
-                        /// remove the old value from the array
-                        $key = array_search($instance, $sequence);
-                        unset($sequence[$key]);
-
-                        /// implode the sequence
-                        $section->sequence = implode(',', $sequence);
-
-                        $DB->set_field('course_sections', 'sequence', $section->sequence, array('id'=>$section->id));
-
-
-                        if ($output) echo 'Online Assignment (instance '.$instance.') moved from section '.$section->id.': to section '.$newsection.'<br />';
-
-                    }
-                }
-            }
-
-            /// if the summary and sequence are empty then remove this section
-            if (empty($xsection->summary) and empty($xsection->sequence)) {
-                $DB->delete_records('course_sections', array('id'=>$xsection->id));
-                if ($output) echo 'Deleting empty section '.$xsection->section.'<br />';
-            }
-        }
-    }
-
-    echo '</center>';
-}
-
-
index ee212b1..8346640 100644 (file)
@@ -67,7 +67,7 @@ if ($roleid && !isset($assignableroles[$roleid])) {
     $a = new stdClass;
     $a->roleid = $roleid;
     $a->context = $contextname;
-    print_error('cannotassignrolehere', '', get_context_url($context), $a);
+    print_error('cannotassignrolehere', '', $context->get_url(), $a);
 }
 
 // Work out an appropriate page title.
index 9c9d7f9..11cadee 100644 (file)
@@ -36,7 +36,7 @@ abstract class core_role_capability_table_base {
     /** The context this table relates to. */
     protected $context;
 
-    /** The capabilities to display. Initialised as fetch_context_capabilities($context). */
+    /** The capabilities to display. Initialised as $context->get_capabilities(). */
     protected $capabilities = array();
 
     /** Added as an id="" attribute to the table on output. */
index a11cbfa..5962355 100644 (file)
@@ -17,9 +17,6 @@ if ($hassiteconfig) {
         $module->load_settings($ADMIN, 'modsettings', $hassiteconfig);
     }
 
-    // hidden script for converting journals to online assignments (or something like that) linked from elsewhere
-    $ADMIN->add('modsettings', new admin_externalpage('oacleanup', 'Online Assignment Cleanup', $CFG->wwwroot.'/'.$CFG->admin.'/oacleanup.php', 'moodle/site:config', true));
-
     // course formats
     $ADMIN->add('modules', new admin_category('formatsettings', new lang_string('courseformats')));
     $temp = new admin_settingpage('manageformats', new lang_string('manageformats', 'core_admin'));
index 9ac6931..5547b56 100644 (file)
@@ -34,6 +34,7 @@ if (function_exists('opcache_reset')) {
 
 // Is not really necessary but adding it as is a CLI_SCRIPT.
 define('CLI_SCRIPT', true);
+define('CACHE_DISABLE_ALL', true);
 
 // Basic functions.
 require_once(__DIR__ . '/../../../../lib/clilib.php');
index b28f4fc..cef4ea7 100644 (file)
@@ -84,6 +84,7 @@ define('BEHAT_UTIL', true);
 define('CLI_SCRIPT', true);
 define('ABORT_AFTER_CONFIG', true);
 define('NO_OUTPUT_BUFFERING', true);
+define('IGNORE_COMPONENT_CACHE', true);
 
 error_reporting(E_ALL | E_STRICT);
 ini_set('display_errors', '1');
index 6c4f296..086e78c 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['aim'] = 'This administration tool helps developers and test writers to create .feature files describing Moodle\'s functionalities and run them automatically.';
 $string['allavailablesteps'] = 'All the available steps definitions';
 $string['giveninfo'] = 'Given. Processes to set up the environment';
+$string['infoheading'] = 'Info';
 $string['installinfo'] = 'Read {$a} for installation and tests execution info';
 $string['moreinfoin'] = 'More info in {$a}';
 $string['newstepsinfo'] = 'Read {$a} for info about how to add new steps definitions';
index 503bc09..db94c85 100644 (file)
@@ -64,9 +64,10 @@ class tool_behat_renderer extends plugin_renderer_base {
             get_string('newstepsinfo', 'tool_behat', $writestepslink)
         );
 
-        // List of steps
+        // List of steps.
         $html .= $this->output->box_start();
-        $html .= html_writer::tag('h1', 'Info');
+        $html .= html_writer::tag('h1', get_string('infoheading', 'tool_behat'));
+        $html .= html_writer::tag('div', get_string('aim', 'tool_behat'));
         $html .= html_writer::empty_tag('div');
         $html .= html_writer::empty_tag('ul');
         $html .= html_writer::empty_tag('li');
index a3d875d..eb807ee 100644 (file)
@@ -32,6 +32,8 @@ if (function_exists('opcache_reset')) {
     opcache_reset();
 }
 
+define('IGNORE_COMPONENT_CACHE', true);
+
 require_once(__DIR__.'/../../../../lib/clilib.php');
 require_once(__DIR__.'/../../../../lib/phpunit/bootstraplib.php');
 require_once(__DIR__.'/../../../../lib/testing/lib.php');
index 035ce7b..be593a2 100644 (file)
@@ -28,6 +28,8 @@ if (isset($_SERVER['REMOTE_ADDR'])) {
     die; // no access from web!
 }
 
+define('IGNORE_COMPONENT_CACHE', true);
+
 require_once(__DIR__.'/../../../../lib/clilib.php');
 require_once(__DIR__.'/../../../../lib/phpunit/bootstraplib.php');
 require_once(__DIR__.'/../../../../lib/testing/lib.php');
diff --git a/admin/tool/uploadcourse/classes/base_form.php b/admin/tool/uploadcourse/classes/base_form.php
new file mode 100644 (file)
index 0000000..fc0ddf9
--- /dev/null
@@ -0,0 +1,93 @@
+<?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/>.
+
+/**
+ * File containing the base import form.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir.'/formslib.php');
+
+/**
+ * Base import form.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_uploadcourse_base_form extends moodleform {
+
+    /**
+     * Empty definition.
+     *
+     * @return void
+     */
+    public function definition() {
+    }
+
+    /**
+     * Adds the import settings part.
+     *
+     * @return void
+     */
+    public function add_import_options() {
+        $mform = $this->_form;
+
+        // Upload settings and file.
+        $mform->addElement('header', 'importoptionshdr', get_string('importoptions', 'tool_uploadcourse'));
+        $mform->setExpanded('importoptionshdr', true);
+
+        $choices = array(
+            tool_uploadcourse_processor::MODE_CREATE_NEW => get_string('createnew', 'tool_uploadcourse'),
+            tool_uploadcourse_processor::MODE_CREATE_ALL => get_string('createall', 'tool_uploadcourse'),
+            tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE => get_string('createorupdate', 'tool_uploadcourse'),
+            tool_uploadcourse_processor::MODE_UPDATE_ONLY => get_string('updateonly', 'tool_uploadcourse')
+        );
+        $mform->addElement('select', 'options[mode]', get_string('mode', 'tool_uploadcourse'), $choices);
+
+        $choices = array(
+            tool_uploadcourse_processor::UPDATE_NOTHING => get_string('nochanges', 'tool_uploadcourse'),
+            tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY => get_string('updatewithdataonly', 'tool_uploadcourse'),
+            tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS =>
+                get_string('updatewithdataordefaults', 'tool_uploadcourse'),
+            tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS => get_string('updatemissing', 'tool_uploadcourse')
+        );
+        $mform->addElement('select', 'options[updatemode]', get_string('updatemode', 'tool_uploadcourse'), $choices);
+        $mform->setDefault('options[updatemode]', tool_uploadcourse_processor::UPDATE_NOTHING);
+        $mform->disabledIf('options[updatemode]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+        $mform->disabledIf('options[updatemode]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+
+        $mform->addElement('selectyesno', 'options[allowdeletes]', get_string('allowdeletes', 'tool_uploadcourse'));
+        $mform->setDefault('options[allowdeletes]', 0);
+        $mform->disabledIf('options[allowdeletes]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+        $mform->disabledIf('options[allowdeletes]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+
+        $mform->addElement('selectyesno', 'options[allowrenames]', get_string('allowrenames', 'tool_uploadcourse'));
+        $mform->setDefault('options[allowrenames]', 0);
+        $mform->disabledIf('options[allowrenames]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+        $mform->disabledIf('options[allowrenames]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+
+        $mform->addElement('selectyesno', 'options[allowresets]', get_string('allowresets', 'tool_uploadcourse'));
+        $mform->setDefault('options[allowresets]', 0);
+        $mform->disabledIf('options[allowresets]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+        $mform->disabledIf('options[allowresets]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+    }
+
+}
diff --git a/admin/tool/uploadcourse/classes/course.php b/admin/tool/uploadcourse/classes/course.php
new file mode 100644 (file)
index 0000000..629e498
--- /dev/null
@@ -0,0 +1,898 @@
+<?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/>.
+
+/**
+ * File containing the course class.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
+require_once($CFG->dirroot . '/course/lib.php');
+
+/**
+ * Course class.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_uploadcourse_course {
+
+    /** Outcome of the process: creating the course */
+    const DO_CREATE = 1;
+
+    /** Outcome of the process: updating the course */
+    const DO_UPDATE = 2;
+
+    /** Outcome of the process: deleting the course */
+    const DO_DELETE = 3;
+
+    /** @var array final import data. */
+    protected $data = array();
+
+    /** @var array default values. */
+    protected $defaults = array();
+
+    /** @var array enrolment data. */
+    protected $enrolmentdata;
+
+    /** @var array errors. */
+    protected $errors = array();
+
+    /** @var int the ID of the course that had been processed. */
+    protected $id;
+
+    /** @var array containing options passed from the processor. */
+    protected $importoptions = array();
+
+    /** @var int import mode. Matches tool_uploadcourse_processor::MODE_* */
+    protected $mode;
+
+    /** @var array course import options. */
+    protected $options = array();
+
+    /** @var int constant value of self::DO_*, what to do with that course */
+    protected $do;
+
+    /** @var bool set to true once we have prepared the course */
+    protected $prepared = false;
+
+    /** @var bool set to true once we have started the process of the course */
+    protected $processstarted = false;
+
+    /** @var array course import data. */
+    protected $rawdata = array();
+
+    /** @var array restore directory. */
+    protected $restoredata;
+
+    /** @var string course shortname. */
+    protected $shortname;
+
+    /** @var array errors. */
+    protected $statuses = array();
+
+    /** @var int update mode. Matches tool_uploadcourse_processor::UPDATE_* */
+    protected $updatemode;
+
+    /** @var array fields allowed as course data. */
+    static protected $validfields = array('fullname', 'shortname', 'idnumber', 'category', 'visible', 'startdate',
+        'summary', 'format', 'theme', 'lang', 'newsitems', 'showgrades', 'showreports', 'legacyfiles', 'maxbytes',
+        'groupmode', 'groupmodeforce', 'groupmodeforce', 'enablecompletion');
+
+    /** @var array fields required on course creation. */
+    static protected $mandatoryfields = array('fullname', 'category');
+
+    /** @var array fields which are considered as options. */
+    static protected $optionfields = array('delete' => false, 'rename' => null, 'backupfile' => null,
+        'templatecourse' => null, 'reset' => false);
+
+    /** @var array options determining what can or cannot be done at an import level. */
+    static protected $importoptionsdefaults = array('canrename' => false, 'candelete' => false, 'canreset' => false,
+        'reset' => false, 'restoredir' => null, 'shortnametemplate' => null);
+
+    /**
+     * Constructor
+     *
+     * @param int $mode import mode, constant matching tool_uploadcourse_processor::MODE_*
+     * @param int $updatemode update mode, constant matching tool_uploadcourse_processor::UPDATE_*
+     * @param array $rawdata raw course data.
+     * @param array $defaults default course data.
+     * @param array $importoptions import options.
+     */
+    public function __construct($mode, $updatemode, $rawdata, $defaults = array(), $importoptions = array()) {
+
+        if ($mode !== tool_uploadcourse_processor::MODE_CREATE_NEW &&
+                $mode !== tool_uploadcourse_processor::MODE_CREATE_ALL &&
+                $mode !== tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE &&
+                $mode !== tool_uploadcourse_processor::MODE_UPDATE_ONLY) {
+            throw new coding_exception('Incorrect mode.');
+        } else if ($updatemode !== tool_uploadcourse_processor::UPDATE_NOTHING &&
+                $updatemode !== tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY &&
+                $updatemode !== tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS &&
+                $updatemode !== tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS) {
+            throw new coding_exception('Incorrect update mode.');
+        }
+
+        $this->mode = $mode;
+        $this->updatemode = $updatemode;
+
+        if (isset($rawdata['shortname'])) {
+            $this->shortname = $rawdata['shortname'];
+        }
+        $this->rawdata = $rawdata;
+        $this->defaults = $defaults;
+
+        // Extract course options.
+        foreach (self::$optionfields as $option => $default) {
+            $this->options[$option] = isset($rawdata[$option]) ? $rawdata[$option] : $default;
+        }
+
+        // Import options.
+        foreach (self::$importoptionsdefaults as $option => $default) {
+            $this->importoptions[$option] = isset($importoptions[$option]) ? $importoptions[$option] : $default;
+        }
+    }
+
+    /**
+     * Does the mode allow for course creation?
+     *
+     * @return bool
+     */
+    public function can_create() {
+        return in_array($this->mode, array(tool_uploadcourse_processor::MODE_CREATE_ALL,
+            tool_uploadcourse_processor::MODE_CREATE_NEW,
+            tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE)
+        );
+    }
+
+    /**
+     * Does the mode allow for course deletion?
+     *
+     * @return bool
+     */
+    public function can_delete() {
+        return $this->importoptions['candelete'];
+    }
+
+    /**
+     * Does the mode only allow for course creation?
+     *
+     * @return bool
+     */
+    public function can_only_create() {
+        return in_array($this->mode, array(tool_uploadcourse_processor::MODE_CREATE_ALL,
+            tool_uploadcourse_processor::MODE_CREATE_NEW));
+    }
+
+    /**
+     * Does the mode allow for course rename?
+     *
+     * @return bool
+     */
+    public function can_rename() {
+        return $this->importoptions['canrename'];
+    }
+
+    /**
+     * Does the mode allow for course reset?
+     *
+     * @return bool
+     */
+    public function can_reset() {
+        return $this->importoptions['canreset'];
+    }
+
+    /**
+     * Does the mode allow for course update?
+     *
+     * @return bool
+     */
+    public function can_update() {
+        return in_array($this->mode,
+                array(
+                    tool_uploadcourse_processor::MODE_UPDATE_ONLY,
+                    tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE)
+                ) && $this->updatemode != tool_uploadcourse_processor::UPDATE_NOTHING;
+    }
+
+    /**
+     * Can we use default values?
+     *
+     * @return bool
+     */
+    public function can_use_defaults() {
+        return in_array($this->updatemode, array(tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS,
+            tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS));
+    }
+
+    /**
+     * Delete the current course.
+     *
+     * @return bool
+     */
+    protected function delete() {
+        global $DB;
+        $this->id = $DB->get_field_select('course', 'id', 'shortname = :shortname',
+            array('shortname' => $this->shortname), MUST_EXIST);
+        return delete_course($this->id, false);
+    }
+
+    /**
+     * Log an error
+     *
+     * @param string $code error code.
+     * @param lang_string $message error message.
+     * @return void
+     */
+    protected function error($code, lang_string $message) {
+        if (array_key_exists($code, $this->errors)) {
+            throw new coding_exception('Error code already defined');
+        }
+        $this->errors[$code] = $message;
+    }
+
+    /**
+     * Return whether the course exists or not.
+     *
+     * @param string $shortname the shortname to use to check if the course exists. Falls back on $this->shortname if empty.
+     * @return bool
+     */
+    protected function exists($shortname = null) {
+        global $DB;
+        if (is_null($shortname)) {
+            $shortname = $this->shortname;
+        }
+        if (!empty($shortname) || is_numeric($shortname)) {
+            return $DB->record_exists('course', array('shortname' => $shortname));
+        }
+        return false;
+    }
+
+    /**
+     * Return the data that will be used upon saving.
+     *
+     * @return null|array
+     */
+    public function get_data() {
+        return $this->data;
+    }
+
+    /**
+     * Return the errors found during preparation.
+     *
+     * @return array
+     */
+    public function get_errors() {
+        return $this->errors;
+    }
+
+    /**
+     * Assemble the course data based on defaults.
+     *
+     * This returns the final data to be passed to create_course().
+     *
+     * @param array $data current data.
+     * @return array
+     */
+    protected function get_final_create_data($data) {
+        foreach (self::$validfields as $field) {
+            if (!isset($data[$field]) && isset($this->defaults[$field])) {
+                $data[$field] = $this->defaults[$field];
+            }
+        }
+        $data['shortname'] = $this->shortname;
+        return $data;
+    }
+
+    /**
+     * Assemble the course data based on defaults.
+     *
+     * This returns the final data to be passed to update_course().
+     *
+     * @param array $data current data.
+     * @param bool $usedefaults are defaults allowed?
+     * @param bool $missingonly ignore fields which are already set.
+     * @return array
+     */
+    protected function get_final_update_data($data, $usedefaults = false, $missingonly = false) {
+        global $DB;
+        $newdata = array();
+        $existingdata = $DB->get_record('course', array('shortname' => $this->shortname));
+        foreach (self::$validfields as $field) {
+            if ($missingonly) {
+                if (!is_null($existingdata->$field) and $existingdata->$field !== '') {
+                    continue;
+                }
+            }
+            if (isset($data[$field])) {
+                $newdata[$field] = $data[$field];
+            } else if ($usedefaults && isset($this->defaults[$field])) {
+                $newdata[$field] = $this->defaults[$field];
+            }
+        }
+        $newdata['id'] =  $existingdata->id;
+        return $newdata;
+    }
+
+    /**
+     * Return the ID of the processed course.
+     *
+     * @return int|null
+     */
+    public function get_id() {
+        if (!$this->processstarted) {
+            throw new coding_exception('The course has not been processed yet!');
+        }
+        return $this->id;
+    }
+
+    /**
+     * Get the directory of the object to restore.
+     *
+     * @return string|false subdirectory in $CFG->tempdir/backup/...
+     */
+    protected function get_restore_content_dir() {
+        $backupfile = null;
+        $shortname = null;
+
+        if (!empty($this->options['backupfile'])) {
+            $backupfile = $this->options['backupfile'];
+        } else if (!empty($this->options['templatecourse']) || is_numeric($this->options['templatecourse'])) {
+            $shortname = $this->options['templatecourse'];
+        }
+
+        $errors = array();
+        $dir = tool_uploadcourse_helper::get_restore_content_dir($backupfile, $shortname, $errors);
+        if (!empty($errors)) {
+            foreach ($errors as $key => $message) {
+                $this->error($key, $message);
+            }
+            return false;
+        }
+
+        if (empty($dir) && !empty($this->importoptions['restoredir'])) {
+            $dir = $this->importoptions['restoredir'];
+        }
+
+        return $dir;
+    }
+
+    /**
+     * Return the errors found during preparation.
+     *
+     * @return array
+     */
+    public function get_statuses() {
+        return $this->statuses;
+    }
+
+    /**
+     * Return whether there were errors with this course.
+     *
+     * @return boolean
+     */
+    public function has_errors() {
+        return !empty($this->errors);
+    }
+
+    /**
+     * Validates and prepares the data.
+     *
+     * @return bool false is any error occured.
+     */
+    public function prepare() {
+        global $DB;
+        $this->prepared = true;
+
+        // Validate the shortname.
+        if (!empty($this->shortname) || is_numeric($this->shortname)) {
+            if ($this->shortname !== clean_param($this->shortname, PARAM_TEXT)) {
+                $this->error('invalidshortname', new lang_string('invalidshortname', 'tool_uploadcourse'));
+                return false;
+            }
+        }
+
+        $exists = $this->exists();
+
+        // Do we want to delete the course?
+        if ($this->options['delete']) {
+            if (!$exists) {
+                $this->error('cannotdeletecoursenotexist', new lang_string('cannotdeletecoursenotexist', 'tool_uploadcourse'));
+                return false;
+            } else if (!$this->can_delete()) {
+                $this->error('coursedeletionnotallowed', new lang_string('coursedeletionnotallowed', 'tool_uploadcourse'));
+                return false;
+            }
+
+            $this->do = self::DO_DELETE;
+            return true;
+        }
+
+        // Can we create/update the course under those conditions?
+        if ($exists) {
+            if ($this->mode === tool_uploadcourse_processor::MODE_CREATE_NEW) {
+                $this->error('courseexistsanduploadnotallowed',
+                    new lang_string('courseexistsanduploadnotallowed', 'tool_uploadcourse'));
+                return false;
+            }
+        } else {
+            if (!$this->can_create()) {
+                $this->error('coursedoesnotexistandcreatenotallowed',
+                    new lang_string('coursedoesnotexistandcreatenotallowed', 'tool_uploadcourse'));
+                return false;
+            }
+        }
+
+        // Basic data.
+        $coursedata = array();
+        foreach ($this->rawdata as $field => $value) {
+            if (!in_array($field, self::$validfields)) {
+                continue;
+            } else if ($field == 'shortname') {
+                // Let's leave it apart from now, use $this->shortname only.
+                continue;
+            }
+            $coursedata[$field] = $value;
+        }
+
+        $mode = $this->mode;
+        $updatemode = $this->updatemode;
+        $usedefaults = $this->can_use_defaults();
+
+        // Resolve the category, and fail if not found.
+        $errors = array();
+        $catid = tool_uploadcourse_helper::resolve_category($this->rawdata, $errors);
+        if (empty($errors)) {
+            $coursedata['category'] = $catid;
+        } else {
+            foreach ($errors as $key => $message) {
+                $this->error($key, $message);
+            }
+            return false;
+        }
+
+        // If the course does not exist, or will be forced created.
+        if (!$exists || $mode === tool_uploadcourse_processor::MODE_CREATE_ALL) {
+
+            // Mandatory fields upon creation.
+            $errors = array();
+            foreach (self::$mandatoryfields as $field) {
+                if ((!isset($coursedata[$field]) || $coursedata[$field] === '') &&
+                        (!isset($this->defaults[$field]) || $this->defaults[$field] === '')) {
+                    $errors[] = $field;
+                }
+            }
+            if (!empty($errors)) {
+                $this->error('missingmandatoryfields', new lang_string('missingmandatoryfields', 'tool_uploadcourse',
+                    implode(', ', $errors)));
+                return false;
+            }
+        }
+
+        // Should the course be renamed?
+        if (!empty($this->options['rename']) || is_numeric($this->options['rename'])) {
+            if (!$this->can_update()) {
+                $this->error('canonlyrenameinupdatemode', new lang_string('canonlyrenameinupdatemode', 'tool_uploadcourse'));
+                return false;
+            } else if (!$exists) {
+                $this->error('cannotrenamecoursenotexist', new lang_string('cannotrenamecoursenotexist', 'tool_uploadcourse'));
+                return false;
+            } else if (!$this->can_rename()) {
+                $this->error('courserenamingnotallowed', new lang_string('courserenamingnotallowed', 'tool_uploadcourse'));
+                return false;
+            } else if ($this->options['rename'] !== clean_param($this->options['rename'], PARAM_TEXT)) {
+                $this->error('invalidshortname', new lang_string('invalidshortname', 'tool_uploadcourse'));
+                return false;
+            } else if ($this->exists($this->options['rename'])) {
+                $this->error('cannotrenameshortnamealreadyinuse',
+                    new lang_string('cannotrenameshortnamealreadyinuse', 'tool_uploadcourse'));
+                return false;
+            } else if (isset($coursedata['idnumber']) &&
+                    $DB->count_records_select('course', 'idnumber = :idn AND shortname != :sn',
+                    array('idn' => $coursedata['idnumber'], 'sn' => $this->shortname)) > 0) {
+                $this->error('cannotrenameidnumberconflict', new lang_string('cannotrenameidnumberconflict', 'tool_uploadcourse'));
+                return false;
+            }
+            $coursedata['shortname'] = $this->options['rename'];
+            $this->status('courserenamed', new lang_string('courserenamed', 'tool_uploadcourse',
+                array('from' => $this->shortname, 'to' => $coursedata['shortname'])));
+        }
+
+        // Should we generate a shortname?
+        if (empty($this->shortname) && !is_numeric($this->shortname)) {
+            if (empty($this->importoptions['shortnametemplate'])) {
+                $this->error('missingshortnamenotemplate', new lang_string('missingshortnamenotemplate', 'tool_uploadcourse'));
+                return false;
+            } else if (!$this->can_only_create()) {
+                $this->error('cannotgenerateshortnameupdatemode',
+                    new lang_string('cannotgenerateshortnameupdatemode', 'tool_uploadcourse'));
+                return false;
+            } else {
+                $newshortname = tool_uploadcourse_helper::generate_shortname($coursedata,
+                    $this->importoptions['shortnametemplate']);
+                if (is_null($newshortname)) {
+                    $this->error('generatedshortnameinvalid', new lang_string('generatedshortnameinvalid', 'tool_uploadcourse'));
+                    return false;
+                } else if ($this->exists($newshortname)) {
+                    if ($mode === tool_uploadcourse_processor::MODE_CREATE_NEW) {
+                        $this->error('generatedshortnamealreadyinuse',
+                            new lang_string('generatedshortnamealreadyinuse', 'tool_uploadcourse'));
+                        return false;
+                    }
+                    $exists = true;
+                }
+                $this->status('courseshortnamegenerated', new lang_string('courseshortnamegenerated', 'tool_uploadcourse',
+                    $newshortname));
+                $this->shortname = $newshortname;
+            }
+        }
+
+        // If exists, but we only want to create courses, increment the shortname.
+        if ($exists && $mode === tool_uploadcourse_processor::MODE_CREATE_ALL) {
+            $original = $this->shortname;
+            $this->shortname = tool_uploadcourse_helper::increment_shortname($this->shortname);
+            $exists = false;
+            if ($this->shortname != $original) {
+                $this->status('courseshortnameincremented', new lang_string('courseshortnameincremented', 'tool_uploadcourse',
+                    array('from' => $original, 'to' => $this->shortname)));
+                if (isset($coursedata['idnumber'])) {
+                    $originalidn = $coursedata['idnumber'];
+                    $coursedata['idnumber'] = tool_uploadcourse_helper::increment_idnumber($coursedata['idnumber']);
+                    if ($originalidn != $coursedata['idnumber']) {
+                        $this->status('courseidnumberincremented', new lang_string('courseidnumberincremented', 'tool_uploadcourse',
+                            array('from' => $originalidn, 'to' => $coursedata['idnumber'])));
+                    }
+                }
+            }
+        }
+
+        // If the course does not exist, ensure that the ID number is not taken.
+        if (!$exists && isset($coursedata['idnumber'])) {
+            if ($DB->count_records_select('course', 'idnumber = :idn', array('idn' => $coursedata['idnumber'])) > 0) {
+                $this->error('idnumberalreadyinuse', new lang_string('idnumberalreadyinuse', 'tool_uploadcourse'));
+                return false;
+            }
+        }
+
+        // Ultimate check mode vs. existence.
+        switch ($mode) {
+            case tool_uploadcourse_processor::MODE_CREATE_NEW:
+            case tool_uploadcourse_processor::MODE_CREATE_ALL:
+                if ($exists) {
+                    $this->error('courseexistsanduploadnotallowed',
+                        new lang_string('courseexistsanduploadnotallowed', 'tool_uploadcourse'));
+                    return false;
+                }
+                break;
+            case tool_uploadcourse_processor::MODE_UPDATE_ONLY:
+                if (!$exists) {
+                    $this->error('coursedoesnotexistandcreatenotallowed',
+                        new lang_string('coursedoesnotexistandcreatenotallowed', 'tool_uploadcourse'));
+                    return false;
+                }
+                // No break!
+            case tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE:
+                if ($exists) {
+                    if ($updatemode === tool_uploadcourse_processor::UPDATE_NOTHING) {
+                        $this->error('updatemodedoessettonothing',
+                            new lang_string('updatemodedoessettonothing', 'tool_uploadcourse'));
+                        return false;
+                    }
+                }
+                break;
+            default:
+                // O_o Huh?! This should really never happen here!
+                $this->error('unknownimportmode', new lang_string('unknownimportmode', 'tool_uploadcourse'));
+                return false;
+        }
+
+        // Get final data.
+        if ($exists) {
+            $missingonly = ($updatemode === tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS);
+            $coursedata = $this->get_final_update_data($coursedata, $usedefaults, $missingonly);
+            $this->do = self::DO_UPDATE;
+        } else {
+            $coursedata = $this->get_final_create_data($coursedata);
+            $this->do = self::DO_CREATE;
+        }
+
+        // Course start date.
+        if (!empty($coursedata['startdate'])) {
+            $coursedata['startdate'] = strtotime($coursedata['startdate']);
+        }
+
+        // Add role renaming.
+        $errors = array();
+        $rolenames = tool_uploadcourse_helper::get_role_names($this->rawdata, $errors);
+        if (!empty($errors)) {
+            foreach ($errors as $key => $message) {
+                $this->error($key, $message);
+            }
+            return false;
+        }
+        foreach ($rolenames as $rolekey => $rolename) {
+            $coursedata[$rolekey] = $rolename;
+        }
+
+        // Some validation.
+        if (!empty($coursedata['format']) && !in_array($coursedata['format'], tool_uploadcourse_helper::get_course_formats())) {
+            $this->error('invalidcourseformat', new lang_string('invalidcourseformat', 'tool_uploadcourse'));
+            return false;
+        }
+
+        // Saving data.
+        $this->data = $coursedata;
+        $this->enrolmentdata = tool_uploadcourse_helper::get_enrolment_data($this->rawdata);
+
+        // Restore data.
+        // TODO log warnings.
+        $this->restoredata = $this->get_restore_content_dir();
+
+        // We can only reset courses when allowed and we are updating the course.
+        if ($this->importoptions['reset'] || $this->options['reset']) {
+            if ($this->do !== self::DO_UPDATE) {
+                $this->error('canonlyresetcourseinupdatemode',
+                    new lang_string('canonlyresetcourseinupdatemode', 'tool_uploadcourse'));
+                return false;
+            } else if (!$this->can_reset()) {
+                $this->error('courseresetnotallowed', new lang_string('courseresetnotallowed', 'tool_uploadcourse'));
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Proceed with the import of the course.
+     *
+     * @return void
+     */
+    public function proceed() {
+        global $CFG, $USER;
+
+        if (!$this->prepared) {
+            throw new coding_exception('The course has not been prepared.');
+        } else if ($this->has_errors()) {
+            throw new moodle_exception('Cannot proceed, errors were detected.');
+        } else if ($this->processstarted) {
+            throw new coding_exception('The process has already been started.');
+        }
+        $this->processstarted = true;
+
+        if ($this->do === self::DO_DELETE) {
+            if ($this->delete()) {
+                $this->status('coursedeleted', new lang_string('coursedeleted', 'tool_uploadcourse'));
+            } else {
+                $this->error('errorwhiledeletingcourse', new lang_string('errorwhiledeletingcourse', 'tool_uploadcourse'));
+            }
+            return true;
+        } else if ($this->do === self::DO_CREATE) {
+            $course = create_course((object) $this->data);
+            $this->id = $course->id;
+            $this->status('coursecreated', new lang_string('coursecreated', 'tool_uploadcourse'));
+        } else if ($this->do === self::DO_UPDATE) {
+            $course = (object) $this->data;
+            update_course($course);
+            $this->id = $course->id;
+            $this->status('courseupdated', new lang_string('courseupdated', 'tool_uploadcourse'));
+        } else {
+            // Strangely the outcome has not been defined, or is unknown!
+            throw new coding_exception('Unknown outcome!');
+        }
+
+        // Restore a course.
+        if (!empty($this->restoredata)) {
+            $rc = new restore_controller($this->restoredata, $course->id, backup::INTERACTIVE_NO,
+                backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING);
+
+            // Check if the format conversion must happen first.
+            if ($rc->get_status() == backup::STATUS_REQUIRE_CONV) {
+                $rc->convert();
+            }
+            if ($rc->execute_precheck()) {
+                $rc->execute_plan();
+                $this->status('courserestored', new lang_string('courserestored', 'tool_uploadcourse'));
+            } else {
+                $this->error('errorwhilerestoringcourse', new lang_string('errorwhilerestoringthecourse', 'tool_uploadcourse'));
+            }
+            $rc->destroy();
+        }
+
+        // Proceed with enrolment data.
+        $this->process_enrolment_data($course);
+
+        // Reset the course.
+        if ($this->importoptions['reset'] || $this->options['reset']) {
+            if ($this->do === self::DO_UPDATE && $this->can_reset()) {
+                $this->reset($course);
+                $this->status('coursereset', new lang_string('coursereset', 'tool_uploadcourse'));
+            }
+        }
+
+        // Mark context as dirty.
+        $context = context_course::instance($course->id);
+        $context->mark_dirty();
+    }
+
+    /**
+     * Add the enrolment data for the course.
+     *
+     * @param object $course course record.
+     * @return void
+     */
+    protected function process_enrolment_data($course) {
+        global $DB;
+
+        $enrolmentdata = $this->enrolmentdata;
+        if (empty($enrolmentdata)) {
+            return;
+        }
+
+        $enrolmentplugins = tool_uploadcourse_helper::get_enrolment_plugins();
+        $instances = enrol_get_instances($course->id, false);
+        foreach ($enrolmentdata as $enrolmethod => $method) {
+
+            $instance = null;
+            foreach ($instances as $i) {
+                if ($i->enrol == $enrolmethod) {
+                    $instance = $i;
+                    break;
+                }
+            }
+
+            $todelete = isset($method['delete']) && $method['delete'];
+            $todisable = isset($method['disable']) && $method['disable'];
+            unset($method['delete']);
+            unset($method['disable']);
+
+            if (!empty($instance) && $todelete) {
+                // Remove the enrolment method.
+                foreach ($instances as $instance) {
+                    if ($instance->enrol == $enrolmethod) {
+                        $plugin = $enrolmentplugins[$instance->enrol];
+                        $plugin->delete_instance($instance);
+                        break;
+                    }
+                }
+            } else if (!empty($instance) && $todisable) {
+                // Disable the enrolment.
+                foreach ($instances as $instance) {
+                    if ($instance->enrol == $enrolmethod) {
+                        $plugin = $enrolmentplugins[$instance->enrol];
+                        $plugin->update_status($instance, ENROL_INSTANCE_DISABLED);
+                        $enrol_updated = true;
+                        break;
+                    }
+                }
+            } else {
+                $plugin = null;
+                if (empty($instance)) {
+                    $plugin = $enrolmentplugins[$enrolmethod];
+                    $instance = new stdClass();
+                    $instance->id = $plugin->add_default_instance($course);
+                    $instance->roleid = $plugin->get_config('roleid');
+                    $instance->status = ENROL_INSTANCE_ENABLED;
+                } else {
+                    $plugin = $enrolmentplugins[$instance->enrol];
+                    $plugin->update_status($instance, ENROL_INSTANCE_ENABLED);
+                }
+
+                // Now update values.
+                foreach ($method as $k => $v) {
+                    $instance->{$k} = $v;
+                }
+
+                // Sort out the start, end and date.
+                $instance->enrolstartdate = (isset($method['startdate']) ? strtotime($method['startdate']) : 0);
+                $instance->enrolenddate = (isset($method['enddate']) ? strtotime($method['enddate']) : 0);
+
+                // Is the enrolment period set?
+                if (isset($method['enrolperiod']) && ! empty($method['enrolperiod'])) {
+                    if (preg_match('/^\d+$/', $method['enrolperiod'])) {
+                        $method['enrolperiod'] = (int) $method['enrolperiod'];
+                    } else {
+                        // Try and convert period to seconds.
+                        $method['enrolperiod'] = strtotime('1970-01-01 GMT + ' . $method['enrolperiod']);
+                    }
+                    $instance->enrolperiod = $method['enrolperiod'];
+                }
+                if ($instance->enrolstartdate > 0 && isset($method['enrolperiod'])) {
+                    $instance->enrolenddate = $instance->enrolstartdate + $method['enrolperiod'];
+                }
+                if ($instance->enrolenddate > 0) {
+                    $instance->enrolperiod = $instance->enrolenddate - $instance->enrolstartdate;
+                }
+                if ($instance->enrolenddate < $instance->enrolstartdate) {
+                    $instance->enrolenddate = $instance->enrolstartdate;
+                }
+
+                // Sort out the given role. This does not filter the roles allowed in the course.
+                if (isset($method['role'])) {
+                    $roleids = tool_uploadcourse_helper::get_role_ids();
+                    if (isset($roleids[$method['role']])) {
+                        $instance->roleid = $roleids[$method['role']];
+                    }
+                }
+
+                $instance->timemodified = time();
+                $DB->update_record('enrol', $instance);
+            }
+        }
+    }
+
+    /**
+     * Reset the current course.
+     *
+     * This does not reset any of the content of the activities.
+     *
+     * @param stdClass $course the course object of the course to reset.
+     * @return array status array of array component, item, error.
+     */
+    protected function reset($course) {
+        global $DB;
+
+        $resetdata = new stdClass();
+        $resetdata->id = $course->id;
+        $resetdata->reset_start_date = time();
+        $resetdata->reset_logs = true;
+        $resetdata->reset_events = true;
+        $resetdata->reset_notes = true;
+        $resetdata->delete_blog_associations = true;
+        $resetdata->reset_completion = true;
+        $resetdata->reset_roles_overrides = true;
+        $resetdata->reset_roles_local = true;
+        $resetdata->reset_groups_members = true;
+        $resetdata->reset_groups_remove = true;
+        $resetdata->reset_groupings_members = true;
+        $resetdata->reset_groupings_remove = true;
+        $resetdata->reset_gradebook_items = true;
+        $resetdata->reset_gradebook_grades = true;
+        $resetdata->reset_comments = true;
+
+        if (empty($course->startdate)) {
+            $course->startdate = $DB->get_field_select('course', 'startdate', 'id = :id', array('id' => $course->id));
+        }
+        $resetdata->reset_start_date_old = $course->startdate;
+
+        // Add roles.
+        $roles = tool_uploadcourse_helper::get_role_ids();
+        $resetdata->unenrol_users = array_values($roles);
+        $resetdata->unenrol_users[] = 0;    // Enrolled without role.
+
+        return reset_course_userdata($resetdata);
+    }
+
+    /**
+     * Log a status
+     *
+     * @param string $code status code.
+     * @param lang_string $message status message.
+     * @return void
+     */
+    protected function status($code, lang_string $message) {
+        if (array_key_exists($code, $this->statuses)) {
+            throw new coding_exception('Status code already defined');
+        }
+        $this->statuses[$code] = $message;
+    }
+
+}
diff --git a/admin/tool/uploadcourse/classes/helper.php b/admin/tool/uploadcourse/classes/helper.php
new file mode 100644 (file)
index 0000000..b328a42
--- /dev/null
@@ -0,0 +1,486 @@
+<?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/>.
+
+/**
+ * File containing the helper class.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir . '/coursecatlib.php');
+require_once($CFG->dirroot . '/cache/lib.php');
+require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
+require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
+
+/**
+ * Class containing a set of helpers.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_uploadcourse_helper {
+
+    /**
+     * Remove the restore content from disk and cache.
+     *
+     * @return void
+     */
+    public static function clean_restore_content() {
+        global $CFG;
+        if (!empty($CFG->keeptempdirectoriesonbackup)) {
+            $cache = cache::make('tool_uploadcourse', 'helper');
+            $backupids = (array) $cache->get('backupids');
+            foreach ($backupids as $cachekey => $backupid) {
+                $cache->delete($cachekey);
+                fulldelete("$CFG->tempdir/backup/$backupid/");
+            }
+            $cache->delete('backupids');
+        }
+    }
+
+    /**
+     * Generate a shortname based on a template.
+     *
+     * @param array|object $data course data.
+     * @param string $templateshortname template of shortname.
+     * @return null|string shortname based on the template, or null when an error occured.
+     */
+    public static function generate_shortname($data, $templateshortname) {
+        if (empty($templateshortname) && !is_numeric($templateshortname)) {
+            return null;
+        }
+        if (strpos($templateshortname, '%') === false) {
+            return $templateshortname;
+        }
+
+        $course = (object) $data;
+        $fullname   = isset($course->fullname) ? $course->fullname : '';
+        $idnumber   = isset($course->idnumber) ? $course->idnumber  : '';
+
+        $callback = partial(array('tool_uploadcourse_helper', 'generate_shortname_callback'), $fullname, $idnumber);
+        $result = preg_replace_callback('/(?<!%)%([+~-])?(\d)*([fi])/', $callback, $templateshortname);
+
+        if (!is_null($result)) {
+            $result = clean_param($result, PARAM_TEXT);
+        }
+
+        if (empty($result) && !is_numeric($result)) {
+            $result = null;
+        }
+
+        return $result;
+    }
+
+    /**
+     * Callback used when generating a shortname based on a template.
+     *
+     * @param string $fullname full name.
+     * @param string $idnumber ID number.
+     * @param array $block result from preg_replace_callback.
+     * @return string
+     */
+    public static function generate_shortname_callback($fullname, $idnumber, $block) {
+        switch ($block[3]) {
+            case 'f':
+                $repl = $fullname;
+                break;
+            case 'i':
+                $repl = $idnumber;
+                break;
+            default:
+                return $block[0];
+        }
+
+        switch ($block[1]) {
+            case '+':
+                $repl = textlib::strtoupper($repl);
+                break;
+            case '-':
+                $repl = textlib::strtolower($repl);
+                break;
+            case '~':
+                $repl = textlib::strtotitle($repl);
+                break;
+        }
+
+        if (!empty($block[2])) {
+            $repl = textlib::substr($repl, 0, $block[2]);
+        }
+
+        return $repl;
+    }
+
+    /**
+     * Return the available course formats.
+     *
+     * @return array
+     */
+    public static function get_course_formats() {
+        return array_keys(core_component::get_plugin_list('format'));
+    }
+
+    /**
+     * Extract enrolment data from passed data.
+     *
+     * Constructs an array of methods, and their options:
+     * array(
+     *     'method1' => array(
+     *         'option1' => value,
+     *         'option2' => value
+     *     ),
+     *     'method2' => array(
+     *         'option1' => value,
+     *         'option2' => value
+     *     )
+     * )
+     *
+     * @param array $data data to extract the enrolment data from.
+     * @return array
+     */
+    public static function get_enrolment_data($data) {
+        $enrolmethods = array();
+        $enroloptions = array();
+        foreach ($data as $field => $value) {
+
+            // Enrolmnent data.
+            $matches = array();
+            if (preg_match('/^enrolment_(\d+)(_(.+))?$/', $field, $matches)) {
+                $key = $matches[1];
+                if (!isset($enroloptions[$key])) {
+                    $enroloptions[$key] = array();
+                }
+                if (empty($matches[3])) {
+                    $enrolmethods[$key] = $value;
+                } else {
+                    $enroloptions[$key][$matches[3]] = $value;
+                }
+            }
+        }
+
+        // Combining enrolment methods and their options in a single array.
+        $enrolmentdata = array();
+        if (!empty($enrolmethods)) {
+            $enrolmentplugins = self::get_enrolment_plugins();
+            foreach ($enrolmethods as $key => $method) {
+                if (!array_key_exists($method, $enrolmentplugins)) {
+                    // Error!
+                    continue;
+                }
+                $enrolmentdata[$enrolmethods[$key]] = $enroloptions[$key];
+            }
+        }
+        return $enrolmentdata;
+    }
+
+    /**
+     * Return the enrolment plugins.
+     *
+     * The result is cached for faster execution.
+     *
+     * @return array
+     */
+    public static function get_enrolment_plugins() {
+        $cache = cache::make('tool_uploadcourse', 'helper');
+        if (($enrol = $cache->get('enrol')) === false) {
+            $enrol = enrol_get_plugins(false);
+            $cache->set('enrol', $enrol);
+        }
+        return $enrol;
+    }
+
+    /**
+     * Get the restore content tempdir.
+     *
+     * The tempdir is the sub directory in which the backup has been extracted.
+     * This caches the result for better performance.
+     *
+     * @param string $backupfile path to a backup file.
+     * @param string $shortname shortname of a course.
+     * @param array $errors will be populated with errors found.
+     * @return string|false false when the backup couldn't retrieved.
+     */
+    public static function get_restore_content_dir($backupfile = null, $shortname = null, &$errors = array()) {
+        global $CFG, $DB, $USER;
+
+        $cachekey = null;
+        if (!empty($backupfile)) {
+            $backupfile = realpath($backupfile);
+            $cachekey = 'backup_path:' . $backupfile;
+        } else if (!empty($shortname) || is_numeric($shortname)) {
+            $cachekey = 'backup_sn:' . $shortname;
+        }
+
+        if (empty($cachekey)) {
+            return false;
+        }
+
+        $cache = cache::make('tool_uploadcourse', 'helper');
+        if (($backupid = $cache->get($cachekey)) === false) {
+            // Use false instead of null because it would consider that the cache
+            // key has not been set.
+            $backupid = false;
+            if (!empty($backupfile)) {
+                if (!is_readable($backupfile)) {
+                    $errors['cannotreadbackupfile'] = new lang_string('cannotreadbackupfile', 'tool_uploadcourse');
+                } else {
+                    // Extracting the backup file.
+                    $packer = get_file_packer('application/vnd.moodle.backup');
+                    $backupid = restore_controller::get_tempdir_name(SITEID, $USER->id);
+                    $path = "$CFG->tempdir/backup/$backupid/";
+                    $result = $packer->extract_to_pathname($backupfile, $path);
+                    if (!$result) {
+                        $errors['invalidbackupfile'] = new lang_string('invalidbackupfile', 'tool_uploadcourse');
+                    }
+                }
+            } else if (!empty($shortname) || is_numeric($shortname)) {
+                // Creating restore from an existing course.
+                $courseid = $DB->get_field('course', 'id', array('shortname' => $shortname), IGNORE_MISSING);
+                if (!empty($courseid)) {
+                    $bc = new backup_controller(backup::TYPE_1COURSE, $courseid, backup::FORMAT_MOODLE,
+                        backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id);
+                    $bc->execute_plan();
+                    $backupid = $bc->get_backupid();
+                    $bc->destroy();
+                } else {
+                    $errors['coursetorestorefromdoesnotexist'] =
+                        new lang_string('coursetorestorefromdoesnotexist', 'tool_uploadcourse');
+                }
+            }
+            $cache->set($cachekey, $backupid);
+
+            // Store all the directories to be able to remove them in self::clean_restore_content().
+            $backupids = (array) $cache->get('backupids');
+            $backupids[$cachekey] = $backupid;
+            $cache->set('backupids', $backupids);
+        }
+
+        return $backupid;
+    }
+
+    /**
+     * Return the role IDs.
+     *
+     * The result is cached for faster execution.
+     *
+     * @return array
+     */
+    public static function get_role_ids() {
+        $cache = cache::make('tool_uploadcourse', 'helper');
+        if (($roles = $cache->get('roles')) === false) {
+            $roles = array();
+            $rolesraw = get_all_roles();
+            foreach ($rolesraw as $role) {
+                $roles[$role->shortname] = $role->id;
+            }
+            $cache->set('roles', $roles);
+        }
+        return $roles;
+    }
+
+    /**
+     * Get the role renaming data from the passed data.
+     *
+     * @param array $data data to extract the names from.
+     * @param array $errors will be populated with errors found.
+     * @return array where the key is the role_<id>, the value is the new name.
+     */
+    public static function get_role_names($data, &$errors = array()) {
+        $rolenames = array();
+        $rolesids = self::get_role_ids();
+        $invalidroles = array();
+        foreach ($data as $field => $value) {
+
+            $matches = array();
+            if (preg_match('/^role_(.+)?$/', $field, $matches)) {
+                if (!isset($rolesids[$matches[1]])) {
+                    $invalidroles[] = $matches[1];
+                    continue;
+                }
+                $rolenames['role_' . $rolesids[$matches[1]]] = $value;
+            }
+
+        }
+
+        if (!empty($invalidroles)) {
+            $errors['invalidroles'] = new lang_string('invalidroles', 'tool_uploadcourse', implode(', ', $invalidroles));
+        }
+
+        // Roles names.
+        return $rolenames;
+    }
+
+    /**
+     * Helper to increment an ID number.
+     *
+     * This first checks if the ID number is in use.
+     *
+     * @param string $idnumber ID number to increment.
+     * @return string new ID number.
+     */
+    public static function increment_idnumber($idnumber) {
+        global $DB;
+        while ($DB->record_exists('course', array('idnumber' => $idnumber))) {
+            $matches = array();
+            if (!preg_match('/(.*?)([0-9]+)$/', $idnumber, $matches)) {
+                $newidnumber = $idnumber . '_2';
+            } else {
+                $newidnumber = $matches[1] . ((int) $matches[2] + 1);
+            }
+            $idnumber = $newidnumber;
+        }
+        return $idnumber;
+    }
+
+    /**
+     * Helper to increment a shortname.
+     *
+     * This considers that the shortname passed has to be incremented.
+     *
+     * @param string $shortname shortname to increment.
+     * @return string new shortname.
+     */
+    public static function increment_shortname($shortname) {
+        global $DB;
+        do {
+            $matches = array();
+            if (!preg_match('/(.*?)([0-9]+)$/', $shortname, $matches)) {
+                $newshortname = $shortname . '_2';
+            } else {
+                $newshortname = $matches[1] . ($matches[2]+1);
+            }
+            $shortname = $newshortname;
+        } while ($DB->record_exists('course', array('shortname' => $shortname)));
+        return $shortname;
+    }
+
+    /**
+     * Resolve a category based on the data passed.
+     *
+     * Key accepted are:
+     * - category, which is supposed to be a category ID.
+     * - category_idnumber
+     * - category_path, array of categories from parent to child.
+     *
+     * @param array $data to resolve the category from.
+     * @param array $errors will be populated with errors found.
+     * @return int category ID.
+     */
+    public static function resolve_category($data, &$errors = array()) {
+        $catid = null;
+
+        if (!empty($data['category'])) {
+            $category = coursecat::get((int) $data['category'], IGNORE_MISSING);
+            if (!empty($category) && !empty($category->id)) {
+                $catid = $category->id;
+            } else {
+                $errors['couldnotresolvecatgorybyid'] =
+                    new lang_string('couldnotresolvecatgorybyid', 'tool_uploadcourse');
+            }
+        }
+
+        if (empty($catid) && !empty($data['category_idnumber'])) {
+            $catid = self::resolve_category_by_idnumber($data['category_idnumber']);
+            if (empty($catid)) {
+                $errors['couldnotresolvecatgorybyidnumber'] =
+                    new lang_string('couldnotresolvecatgorybyidnumber', 'tool_uploadcourse');
+            }
+        }
+        if (empty($catid) && !empty($data['category_path'])) {
+            $catid = self::resolve_category_by_path(explode(' / ', $data['category_path']));
+            if (empty($catid)) {
+                $errors['couldnotresolvecatgorybypath'] =
+                    new lang_string('couldnotresolvecatgorybypath', 'tool_uploadcourse');
+            }
+        }
+
+        return $catid;
+    }
+
+    /**
+     * Resolve a category by ID number.
+     *
+     * @param string $idnumber category ID number.
+     * @return int category ID.
+     */
+    public static function resolve_category_by_idnumber($idnumber) {
+        global $DB;
+        $cache = cache::make('tool_uploadcourse', 'helper');
+        $cachekey = 'cat_idn_' . $idnumber;
+        if (($id = $cache->get($cachekey)) === false) {
+            $params = array('idnumber' => $idnumber);
+            $id = $DB->get_field_select('course_categories', 'id', 'idnumber = :idnumber', $params, IGNORE_MISSING);
+
+            // Little hack to be able to differenciate between the cache not set and a category not found.
+            if ($id === false) {
+                $id = -1;
+            }
+
+            $cache->set($cachekey, $id);
+        }
+
+        // Little hack to be able to differenciate between the cache not set and a category not found.
+        if ($id == -1) {
+            $id = false;
+        }
+
+        return $id;
+    }
+
+    /**
+     * Resolve a category by path.
+     *
+     * @param array $path category names indexed from parent to children.
+     * @return int category ID.
+     */
+    public static function resolve_category_by_path(array $path) {
+        global $DB;
+        $cache = cache::make('tool_uploadcourse', 'helper');
+        $cachekey = 'cat_path_' . serialize($path);
+        if (($id = $cache->get($cachekey)) === false) {
+            $parent = 0;
+            $sql = 'name = :name AND parent = :parent';
+            while ($name = array_shift($path)) {
+                $params = array('name' => $name, 'parent' => $parent);
+                if ($records = $DB->get_records_select('course_categories', $sql, $params, null, 'id, parent')) {
+                    if (count($records) > 1) {
+                        // Too many records with the same name!
+                        $id = -1;
+                        break;
+                    }
+                    $record = reset($records);
+                    $id = $record->id;
+                    $parent = $record->id;
+                } else {
+                    // Not found.
+                    $id = -1;
+                    break;
+                }
+            }
+            $cache->set($cachekey, $id);
+        }
+
+        // We save -1 when the category has not been found to be able to know if the cache was set.
+        if ($id == -1) {
+            $id = false;
+        }
+        return $id;
+    }
+
+}
diff --git a/admin/tool/uploadcourse/classes/processor.php b/admin/tool/uploadcourse/classes/processor.php
new file mode 100644 (file)
index 0000000..94ab8a7
--- /dev/null
@@ -0,0 +1,390 @@
+<?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/>.
+
+/**
+ * File containing processor class.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir . '/csvlib.class.php');
+
+/**
+ * Processor class.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_uploadcourse_processor {
+
+    /**
+     * Create courses that do not exist yet.
+     */
+    const MODE_CREATE_NEW = 1;
+
+    /**
+     * Create all courses, appending a suffix to the shortname if the course exists.
+     */
+    const MODE_CREATE_ALL = 2;
+
+    /**
+     * Create courses, and update the ones that already exist.
+     */
+    const MODE_CREATE_OR_UPDATE = 3;
+
+    /**
+     * Only update existing courses.
+     */
+    const MODE_UPDATE_ONLY = 4;
+
+    /**
+     * During update, do not update anything... O_o Huh?!
+     */
+    const UPDATE_NOTHING = 0;
+
+    /**
+     * During update, only use data passed from the CSV.
+     */
+    const UPDATE_ALL_WITH_DATA_ONLY = 1;
+
+    /**
+     * During update, use either data from the CSV, or defaults.
+     */
+    const UPDATE_ALL_WITH_DATA_OR_DEFAUTLS = 2;
+
+    /**
+     * During update, update missing values from either data from the CSV, or defaults.
+     */
+    const UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS = 3;
+
+    /** @var int processor mode. */
+    protected $mode;
+
+    /** @var int upload mode. */
+    protected $updatemode;
+
+    /** @var bool are renames allowed. */
+    protected $allowrenames = false;
+
+    /** @var bool are deletes allowed. */
+    protected $allowdeletes = false;
+
+    /** @var bool are resets allowed. */
+    protected $allowresets = false;
+
+    /** @var string path to a restore file. */
+    protected $restorefile;
+
+    /** @var string shortname of the course to be restored. */
+    protected $templatecourse;
+
+    /** @var string reset courses after processing them. */
+    protected $reset;
+
+    /** @var string template to generate a course shortname. */
+    protected $shortnametemplate;
+
+    /** @var csv_import_reader */
+    protected $cir;
+
+    /** @var array default values. */
+    protected $defaults = array();
+
+    /** @var array CSV columns. */
+    protected $columns = array();
+
+    /** @var array of errors where the key is the line number. */
+    protected $errors = array();
+
+    /** @var int line number. */
+    protected $linenb = 0;
+
+    /** @var bool whether the process has been started or not. */
+    protected $processstarted = false;
+
+    /**
+     * Constructor
+     *
+     * @param csv_import_reader $cir import reader object
+     * @param array $options options of the process
+     * @param array $defaults default data value
+     */
+    public function __construct(csv_import_reader $cir, array $options, array $defaults = array()) {
+
+        if (!isset($options['mode']) || !in_array($options['mode'], array(self::MODE_CREATE_NEW, self::MODE_CREATE_ALL,
+                self::MODE_CREATE_OR_UPDATE, self::MODE_UPDATE_ONLY))) {
+            throw new coding_exception('Unknown process mode');
+        }
+
+        // Force int to make sure === comparison work as expected.
+        $this->mode = (int) $options['mode'];
+
+        $this->updatemode = self::UPDATE_NOTHING;
+        if (isset($options['updatemode'])) {
+            // Force int to make sure === comparison work as expected.
+            $this->updatemode = (int) $options['updatemode'];
+        }
+        if (isset($options['allowrenames'])) {
+            $this->allowrenames = $options['allowrenames'];
+        }
+        if (isset($options['allowdeletes'])) {
+            $this->allowdeletes = $options['allowdeletes'];
+        }
+        if (isset($options['allowresets'])) {
+            $this->allowresets = $options['allowresets'];
+        }
+
+        if (isset($options['restorefile'])) {
+            $this->restorefile = $options['restorefile'];
+        }
+        if (isset($options['templatecourse'])) {
+            $this->templatecourse = $options['templatecourse'];
+        }
+        if (isset($options['reset'])) {
+            $this->reset = $options['reset'];
+        }
+        if (isset($options['shortnametemplate'])) {
+            $this->shortnametemplate = $options['shortnametemplate'];
+        }
+
+        $this->cir = $cir;
+        $this->columns = $cir->get_columns();
+        $this->defaults = $defaults;
+        $this->validate();
+        $this->reset();
+    }
+
+    /**
+     * Execute the process.
+     *
+     * @param object $tracker the output tracker to use.
+     * @return void
+     */
+    public function execute($tracker = null) {
+        if ($this->processstarted) {
+            throw new coding_exception('Process has already been started');
+        }
+        $this->processstarted = true;
+
+        if (empty($tracker)) {
+            $tracker = new tool_uploadcourse_tracker(tool_uploadcourse_tracker::NO_OUTPUT);
+        }
+        $tracker->start();
+
+        $total = 0;
+        $created = 0;
+        $updated = 0;
+        $deleted = 0;
+        $errors = 0;
+
+        // Loop over the CSV lines.
+        while ($line = $this->cir->next()) {
+            $this->linenb++;
+            $total++;
+
+            $data = $this->parse_line($line);
+            $course = $this->get_course($data);
+            if ($course->prepare()) {
+                $course->proceed();
+
+                $status = $course->get_statuses();
+                if (array_key_exists('coursecreated', $status)) {
+                    $created++;
+                } else if (array_key_exists('courseupdated', $status)) {
+                    $updated++;
+                } else if (array_key_exists('coursedeleted', $status)) {
+                    $deleted++;
+                }
+
+                $data = array_merge($data, $course->get_data(), array('id' => $course->get_id()));
+                $tracker->output($this->linenb, true, $status, $data);
+            } else {
+                $errors++;
+                $tracker->output($this->linenb, false, $course->get_errors(), $data);
+            }
+        }
+
+        $tracker->finish();
+        $tracker->results($total, $created, $updated, $deleted, $errors);
+
+        $this->remove_restore_content();
+    }
+
+    /**
+     * Return a course import object.
+     *
+     * @param array $data data to import the course with.
+     * @return tool_uploadcourse_course
+     */
+    protected function get_course($data) {
+        $importoptions = array(
+            'candelete' => $this->allowdeletes,
+            'canrename' => $this->allowrenames,
+            'canreset' => $this->allowresets,
+            'reset' => $this->reset,
+            'restoredir' => $this->get_restore_content_dir(),
+            'shortnametemplate' => $this->shortnametemplate
+        );
+        return new tool_uploadcourse_course($this->mode, $this->updatemode, $data, $this->defaults, $importoptions);
+    }
+
+    /**
+     * Return the errors.
+     *
+     * @return array
+     */
+    public function get_errors() {
+        return $this->errors;
+    }
+
+    /**
+     * Get the directory of the object to restore.
+     *
+     * @return string subdirectory in $CFG->tempdir/backup/...
+     */
+    protected function get_restore_content_dir() {
+        $backupfile = null;
+        $shortname = null;
+
+        if (!empty($this->restorefile)) {
+            $backupfile = $this->restorefile;
+        } else if (!empty($this->templatecourse) || is_numeric($this->templatecourse)) {
+            $shortname = $this->templatecourse;
+        }
+
+        $dir = tool_uploadcourse_helper::get_restore_content_dir($backupfile, $shortname);
+        return $dir;
+    }
+
+    /**
+     * Log errors on the current line.
+     *
+     * @param array $errors array of errors
+     * @return void
+     */
+    protected function log_error($errors) {
+        if (empty($errors)) {
+            return;
+        }
+
+        foreach ($errors as $code => $langstring) {
+            if (!isset($this->errors[$this->linenb])) {
+                $this->errors[$this->linenb] = array();
+            }
+            $this->errors[$this->linenb][$code] = $langstring;
+        }
+    }
+
+    /**
+     * Parse a line to return an array(column => value)
+     *
+     * @param array $line returned by csv_import_reader
+     * @return array
+     */
+    protected function parse_line($line) {
+        $data = array();
+        foreach ($line as $keynum => $value) {
+            if (!isset($this->columns[$keynum])) {
+                // This should not happen.
+                continue;
+            }
+
+            $key = $this->columns[$keynum];
+            $data[$key] = $value;
+        }
+        return $data;
+    }
+
+    /**
+     * Return a preview of the import.
+     *
+     * This only returns passed data, along with the errors.
+     *
+     * @param integer $rows number of rows to preview.
+     * @param object $tracker the output tracker to use.
+     * @return array of preview data.
+     */
+    public function preview($rows = 10, $tracker = null) {
+        if ($this->processstarted) {
+            throw new coding_exception('Process has already been started');
+        }
+        $this->processstarted = true;
+
+        if (empty($tracker)) {
+            $tracker = new tool_uploadcourse_tracker(tool_uploadcourse_tracker::NO_OUTPUT);
+        }
+        $tracker->start();
+
+        // Loop over the CSV lines.
+        $preview = array();
+        while (($line = $this->cir->next()) && $rows > $this->linenb) {
+            $this->linenb++;
+            $data = $this->parse_line($line);
+            $course = $this->get_course($data);
+            $result = $course->prepare();
+            if (!$result) {
+                $tracker->output($this->linenb, $result, $course->get_errors(), $data);
+            } else {
+                $tracker->output($this->linenb, $result, $course->get_statuses(), $data);
+            }
+            $row = $data;
+            $preview[$this->linenb] = $row;
+        }
+
+        $tracker->finish();
+        $this->remove_restore_content();
+
+        return $preview;
+    }
+
+    /**
+     * Delete the restore object.
+     *
+     * @return void
+     */
+    protected function remove_restore_content() {
+        tool_uploadcourse_helper::clean_restore_content();
+    }
+
+    /**
+     * Reset the current process.
+     *
+     * @return void.
+     */
+    public function reset() {
+        $this->processstarted = false;
+        $this->linenb = 0;
+        $this->cir->init();
+        $this->errors = array();
+    }
+
+    /**
+     * Validation.
+     *
+     * @return void
+     */
+    protected function validate() {
+        if (empty($this->columns)) {
+            throw new moodle_exception('cannotreadtmpfile', 'error');
+        } else if (count($this->columns) < 2) {
+            throw new moodle_exception('csvfewcolumns', 'error');
+        }
+    }
+}
diff --git a/admin/tool/uploadcourse/classes/step1_form.php b/admin/tool/uploadcourse/classes/step1_form.php
new file mode 100644 (file)
index 0000000..cd0abc0
--- /dev/null
@@ -0,0 +1,75 @@
+<?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/>.
+
+/**
+ * File containing the step 1 of the upload form.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/formslib.php');
+
+/**
+ * Upload a file CVS file with course information.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2011 Piers Harding
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_uploadcourse_step1_form extends tool_uploadcourse_base_form {
+
+    /**
+     * The standard form definiton.
+     * @return void
+     */
+    public function definition () {
+        $mform = $this->_form;
+
+        $mform->addElement('header', 'generalhdr', get_string('general'));
+
+        $mform->addElement('filepicker', 'coursefile', get_string('file'));
+        $mform->addRule('coursefile', null, 'required');
+
+        $choices = csv_import_reader::get_delimiter_list();
+        $mform->addElement('select', 'delimiter_name', get_string('csvdelimiter', 'tool_uploadcourse'), $choices);
+        if (array_key_exists('cfg', $choices)) {
+            $mform->setDefault('delimiter_name', 'cfg');
+        } else if (get_string('listsep', 'langconfig') == ';') {
+            $mform->setDefault('delimiter_name', 'semicolon');
+        } else {
+            $mform->setDefault('delimiter_name', 'comma');
+        }
+
+        $choices = textlib::get_encodings();
+        $mform->addElement('select', 'encoding', get_string('encoding', 'tool_uploadcourse'), $choices);
+        $mform->setDefault('encoding', 'UTF-8');
+
+        $choices = array('10' => 10, '20' => 20, '100' => 100, '1000' => 1000, '100000' => 100000);
+        $mform->addElement('select', 'previewrows', get_string('rowpreviewnum', 'tool_uploadcourse'), $choices);
+        $mform->setType('previewrows', PARAM_INT);
+
+        $this->add_import_options();
+
+        $mform->addElement('hidden', 'showpreview', 1);
+        $mform->setType('showpreview', PARAM_INT);
+
+        $this->add_action_buttons(false, get_string('preview', 'tool_uploadcourse'));
+    }
+}
diff --git a/admin/tool/uploadcourse/classes/step2_form.php b/admin/tool/uploadcourse/classes/step2_form.php
new file mode 100644 (file)
index 0000000..7464e4e
--- /dev/null
@@ -0,0 +1,220 @@
+<?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/>.
+
+/**
+ * Bulk course upload step 2.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2011 Piers Harding
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Specify course upload details.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2011 Piers Harding
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
+
+    /**
+     * The standard form definiton.
+     * @return void.
+     */
+    public function definition () {
+        global $CFG;
+
+        $mform   = $this->_form;
+        $data    = $this->_customdata['data'];
+        $courseconfig = get_config('moodlecourse');
+
+        // Import options.
+        $this->add_import_options();
+
+        // Course options.
+        $mform->addElement('header', 'courseoptionshdr', get_string('courseprocess', 'tool_uploadcourse'));
+        $mform->setExpanded('courseoptionshdr', true);
+
+        $mform->addElement('text', 'options[shortnametemplate]', get_string('shortnametemplate', 'tool_uploadcourse'),
+            'maxlength="100" size="20"');
+        $mform->setType('options[shortnametemplate]', PARAM_RAW);
+        $mform->addHelpButton('options[shortnametemplate]', 'shortnametemplate', 'tool_uploadcourse');
+        $mform->disabledIf('options[shortnametemplate]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE);
+        $mform->disabledIf('options[shortnametemplate]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_UPDATE_ONLY);
+
+        $contextid = $this->_customdata['contextid'];
+        $mform->addElement('hidden', 'contextid', $contextid);
+        $mform->setType('contextid', PARAM_INT);
+        $mform->addElement('filepicker', 'options[restorefile]', get_string('templatefile', 'tool_uploadcourse'));
+        $mform->addHelpButton('options[restorefile]', 'templatefile', 'tool_uploadcourse');
+
+        $mform->addElement('text', 'options[templatecourse]', get_string('coursetemplatename', 'tool_uploadcourse'));
+        $mform->setType('options[templatecourse]', PARAM_TEXT);
+        $mform->addHelpButton('options[templatecourse]', 'coursetemplatename', 'tool_uploadcourse');
+
+        $mform->addElement('selectyesno', 'options[reset]', get_string('reset', 'tool_uploadcourse'));
+        $mform->setDefault('options[reset]', 0);
+        $mform->disabledIf('options[reset]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+        $mform->disabledIf('options[reset]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+        $mform->disabledIf('options[reset]', 'options[allowresets]', 'eq', 0);
+
+        // Default values.
+        $mform->addElement('header', 'defaultheader', get_string('defaultvalues', 'tool_uploadcourse'));
+        $mform->setExpanded('defaultheader', true);
+
+        $displaylist = coursecat::make_categories_list('moodle/course:create');
+        $mform->addElement('select', 'defaults[category]', get_string('coursecategory'), $displaylist);
+        $mform->addHelpButton('defaults[category]', 'coursecategory');
+
+        $choices = array();
+        $choices['0'] = get_string('hide');
+        $choices['1'] = get_string('show');
+        $mform->addElement('select', 'defaults[visible]', get_string('visible'), $choices);
+        $mform->addHelpButton('defaults[visible]', 'visible');
+        $mform->setDefault('defaults[visible]', $courseconfig->visible);
+
+        $mform->addElement('date_selector', 'defaults[startdate]', get_string('startdate'));
+        $mform->addHelpButton('defaults[startdate]', 'startdate');
+        $mform->setDefault('defaults[startdate]', time() + 3600 * 24);
+
+        $courseformats = get_sorted_course_formats(true);
+        $formcourseformats = array();
+        foreach ($courseformats as $courseformat) {
+            $formcourseformats[$courseformat] = get_string('pluginname', "format_$courseformat");
+        }
+        $mform->addElement('select', 'defaults[format]', get_string('format'), $formcourseformats);
+        $mform->addHelpButton('defaults[format]', 'format');
+        $mform->setDefault('defaults[format]', $courseconfig->format);
+
+        if (!empty($CFG->allowcoursethemes)) {
+            $themeobjects = get_list_of_themes();
+            $themes=array();
+            $themes[''] = get_string('forceno');
+            foreach ($themeobjects as $key => $theme) {
+                if (empty($theme->hidefromselector)) {
+                    $themes[$key] = get_string('pluginname', 'theme_'.$theme->name);
+                }
+            }
+            $mform->addElement('select', 'defaults[theme]', get_string('forcetheme'), $themes);
+        }
+
+        $languages = array();
+        $languages[''] = get_string('forceno');
+        $languages += get_string_manager()->get_list_of_translations();
+        $mform->addElement('select', 'defaults[lang]', get_string('forcelanguage'), $languages);
+        $mform->setDefault('defaults[lang]', $courseconfig->lang);
+
+        $options = range(0, 10);
+        $mform->addElement('select', 'defaults[newsitems]', get_string('newsitemsnumber'), $options);
+        $mform->addHelpButton('defaults[newsitems]', 'newsitemsnumber');
+        $mform->setDefault('defaults[newsitems]', $courseconfig->newsitems);
+
+        $mform->addElement('selectyesno', 'defaults[showgrades]', get_string('showgrades'));
+        $mform->addHelpButton('defaults[showgrades]', 'showgrades');
+        $mform->setDefault('defaults[showgrades]', $courseconfig->showgrades);
+
+        $mform->addElement('selectyesno', 'defaults[showreports]', get_string('showreports'));
+        $mform->addHelpButton('defaults[showreports]', 'showreports');
+        $mform->setDefault('defaults[showreports]', $courseconfig->showreports);
+
+        if (!empty($CFG->legacyfilesinnewcourses)) {
+            $mform->addElement('select', 'defaults[legacyfiles]', get_string('courselegacyfiles'), $choices);
+            $mform->addHelpButton('defaults[legacyfiles]', 'courselegacyfiles');
+            if (!isset($courseconfig->legacyfiles)) {
+                $courseconfig->legacyfiles = 0;
+            }
+            $mform->setDefault('defaults[legacyfiles]', $courseconfig->legacyfiles);
+        }
+
+        $choices = get_max_upload_sizes($CFG->maxbytes);
+        $mform->addElement('select', 'defaults[maxbytes]', get_string('maximumupload'), $choices);
+        $mform->addHelpButton('defaults[maxbytes]', 'maximumupload');
+        $mform->setDefault('defaults[maxbytes]', $courseconfig->maxbytes);
+
+        $choices = array();
+        $choices[NOGROUPS] = get_string('groupsnone', 'group');
+        $choices[SEPARATEGROUPS] = get_string('groupsseparate', 'group');
+        $choices[VISIBLEGROUPS] = get_string('groupsvisible', 'group');
+        $mform->addElement('select', 'defaults[groupmode]', get_string('groupmode', 'group'), $choices);
+        $mform->addHelpButton('defaults[groupmode]', 'groupmode', 'group');
+        $mform->setDefault('defaults[groupmode]', $courseconfig->groupmode);
+
+        $mform->addElement('selectyesno', 'defaults[groupmodeforce]', get_string('groupmodeforce', 'group'));
+        $mform->addHelpButton('defaults[groupmodeforce]', 'groupmodeforce', 'group');
+        $mform->setDefault('defaults[groupmodeforce]', $courseconfig->groupmodeforce);
+
+        // Hidden fields.
+        $mform->addElement('hidden', 'importid');
+        $mform->setType('importid', PARAM_INT);
+
+        $mform->addElement('hidden', 'previewrows');
+        $mform->setType('previewrows', PARAM_INT);
+
+        $this->add_action_buttons(true, get_string('uploadcourses', 'tool_uploadcourse'));
+
+        $this->set_data($data);
+    }
+
+    /**
+     * Add actopm buttons.
+     *
+     * @param bool $cancel whether to show cancel button, default true
+     * @param string $submitlabel label for submit button, defaults to get_string('savechanges')
+     * @return void
+     */
+    public function add_action_buttons($cancel = true, $submitlabel = null) {
+        $mform =& $this->_form;
+        $buttonarray = array();
+        $buttonarray[] = &$mform->createElement('submit', 'showpreview', get_string('preview', 'tool_uploadcourse'));
+        $buttonarray[] = &$mform->createElement('submit', 'submitbutton', $submitlabel);
+        $buttonarray[] = &$mform->createElement('cancel');
+        $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
+        $mform->closeHeaderBefore('buttonar');
+    }
+
+    /**
+     * Server side validation.
+     * @param array $data - form data
+     * @param object $files  - form files
+     * @return array $errors - form errors
+     */
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+        $columns = $this->_customdata['columns'];
+        $optype  = $data['options']['mode'];
+
+        // Look for other required data.
+        if ($optype != tool_uploadcourse_processor::MODE_UPDATE_ONLY) {
+            if (!in_array('fullname', $columns)) {
+                if (isset($errors['mode'])) {
+                    $errors['mode'] .= ' ';
+                }
+                $errors['mode'] .= get_string('missingfield', 'error', 'fullname');
+            }
+            if (!in_array('summary', $columns)) {
+                if (isset($errors['mode'])) {
+                    $errors['mode'] .= ' ';
+                }
+                $errors['mode'] .= get_string('missingfield', 'error', 'summary');
+            }
+        }
+
+        return $errors;
+    }
+}
diff --git a/admin/tool/uploadcourse/classes/tracker.php b/admin/tool/uploadcourse/classes/tracker.php
new file mode 100644 (file)
index 0000000..0daa845
--- /dev/null
@@ -0,0 +1,220 @@
+<?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/>.
+
+/**
+ * Output tracker.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir . '/weblib.php');
+
+/**
+ * Class output tracker.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_uploadcourse_tracker {
+
+    /**
+     * Constant to output nothing.
+     */
+    const NO_OUTPUT = 0;
+
+    /**
+     * Constant to output HTML.
+     */
+    const OUTPUT_HTML = 1;
+
+    /**
+     * Constant to output plain text.
+     */
+    const OUTPUT_PLAIN = 2;
+
+    /**
+     * @var array columns to display.
+     */
+    protected $columns = array('line', 'result', 'id', 'shortname', 'fullname', 'idnumber', 'status');
+
+    /**
+     * @var int row number.
+     */
+    protected $rownb = 0;
+
+    /**
+     * @var int chosen output mode.
+     */
+    protected $outputmode;
+
+    /**
+     * @var object output buffer.
+     */
+    protected $buffer;
+
+    /**
+     * Constructor.
+     *
+     * @param int $outputmode desired output mode.
+     */
+    public function __construct($outputmode = self::NO_OUTPUT) {
+        $this->outputmode = $outputmode;
+        if ($this->outputmode == self::OUTPUT_PLAIN) {
+            $this->buffer = new progress_trace_buffer(new text_progress_trace());
+        }
+    }
+
+    /**
+     * Finish the output.
+     *
+     * @return void
+     */
+    public function finish() {
+        if ($this->outputmode == self::NO_OUTPUT) {
+            return;
+        }
+
+        if ($this->outputmode == self::OUTPUT_HTML) {
+            echo html_writer::end_tag('table');
+        }
+    }
+
+    /**
+     * Output the results.
+     *
+     * @param int $total total courses.
+     * @param int $created count of courses created.
+     * @param int $updated count of courses updated.
+     * @param int $deleted count of courses deleted.
+     * @param int $errors count of errors.
+     * @return void
+     */
+    public function results($total, $created, $updated, $deleted, $errors) {
+        if ($this->outputmode == self::NO_OUTPUT) {
+            return;
+        }
+
+        $message = array(
+            get_string('coursestotal', 'tool_uploadcourse', $total),
+            get_string('coursescreated', 'tool_uploadcourse', $created),
+            get_string('coursesupdated', 'tool_uploadcourse', $updated),
+            get_string('coursesdeleted', 'tool_uploadcourse', $deleted),
+            get_string('courseserrors', 'tool_uploadcourse', $errors)
+        );
+
+        if ($this->outputmode == self::OUTPUT_PLAIN) {
+            foreach ($message as $msg) {
+                $this->buffer->output($msg);
+            }
+        } else if ($this->outputmode == self::OUTPUT_HTML) {
+            $buffer = new progress_trace_buffer(new html_list_progress_trace());
+            foreach ($message as $msg) {
+                $buffer->output($msg);
+            }
+            $buffer->finished();
+        }
+    }
+
+    /**
+     * Output one more line.
+     *
+     * @param int $line line number.
+     * @param bool $outcome success or not?
+     * @param array $status array of statuses.
+     * @param array $data extra data to display.
+     * @return void
+     */
+    public function output($line, $outcome, $status, $data) {
+        global $OUTPUT;
+        if ($this->outputmode == self::NO_OUTPUT) {
+            return;
+        }
+
+        if ($this->outputmode == self::OUTPUT_PLAIN) {
+            $message = array(
+                $line,
+                $outcome ? 'OK' : 'NOK',
+                isset($data['id']) ? $data['id'] : '',
+                isset($data['shortname']) ? $data['shortname'] : '',
+                isset($data['fullname']) ? $data['fullname'] : '',
+                isset($data['idnumber']) ? $data['idnumber'] : ''
+            );
+            $this->buffer->output(implode("\t", $message));
+            if (!empty($status)) {
+                foreach ($status as $st) {
+                    $this->buffer->output($st, 1);
+                }
+            }
+        } else if ($this->outputmode == self::OUTPUT_HTML) {
+            $ci = 0;
+            $this->rownb++;
+            if (is_array($status)) {
+                $status = implode(html_writer::empty_tag('br'), $status);
+            }
+            if ($outcome) {
+                $outcome = $OUTPUT->pix_icon('i/valid', '');
+            } else {
+                $outcome = $OUTPUT->pix_icon('i/invalid', '');
+            }
+            echo html_writer::start_tag('tr', array('class' => 'r' . $this->rownb % 2));
+            echo html_writer::tag('td', $line, array('class' => 'c' . $ci++));
+            echo html_writer::tag('td', $outcome, array('class' => 'c' . $ci++));
+            echo html_writer::tag('td', isset($data['id']) ? $data['id'] : '', array('class' => 'c' . $ci++));
+            echo html_writer::tag('td', isset($data['shortname']) ? $data['shortname'] : '', array('class' => 'c' . $ci++));
+            echo html_writer::tag('td', isset($data['fullname']) ? $data['fullname'] : '', array('class' => 'c' . $ci++));
+            echo html_writer::tag('td', isset($data['idnumber']) ? $data['idnumber'] : '', array('class' => 'c' . $ci++));
+            echo html_writer::tag('td', $status, array('class' => 'c' . $ci++));
+            echo html_writer::end_tag('tr');
+        }
+    }
+
+    /**
+     * Start the output.
+     *
+     * @return void
+     */
+    public function start() {
+        if ($this->outputmode == self::NO_OUTPUT) {
+            return;
+        }
+
+        if ($this->outputmode == self::OUTPUT_PLAIN) {
+            $columns = array_flip($this->columns);
+            unset($columns['status']);
+            $columns = array_flip($columns);
+            $this->buffer->output(implode("\t", $columns));
+        } else if ($this->outputmode == self::OUTPUT_HTML) {
+            $ci = 0;
+            echo html_writer::start_tag('table', array('class' => 'generaltable boxaligncenter flexible-wrap',
+                'summary' => get_string('uploadcoursesresult', 'tool_uploadcourse')));
+            echo html_writer::start_tag('tr', array('class' => 'heading r' . $this->rownb));
+            echo html_writer::tag('th', get_string('csvline', 'tool_uploadcourse'),
+                array('class' => 'c' . $ci++, 'scope' => 'col'));
+            echo html_writer::tag('th', get_string('result', 'tool_uploadcourse'), array('class' => 'c' . $ci++, 'scope' => 'col'));
+            echo html_writer::tag('th', get_string('id', 'tool_uploadcourse'), array('class' => 'c' . $ci++, 'scope' => 'col'));
+            echo html_writer::tag('th', get_string('shortname'), array('class' => 'c' . $ci++, 'scope' => 'col'));
+            echo html_writer::tag('th', get_string('fullname'), array('class' => 'c' . $ci++, 'scope' => 'col'));
+            echo html_writer::tag('th', get_string('idnumber'), array('class' => 'c' . $ci++, 'scope' => 'col'));
+            echo html_writer::tag('th', get_string('status'), array('class' => 'c' . $ci++, 'scope' => 'col'));
+            echo html_writer::end_tag('tr');
+        }
+    }
+
+}
diff --git a/admin/tool/uploadcourse/cli/uploadcourse.php b/admin/tool/uploadcourse/cli/uploadcourse.php
new file mode 100644 (file)
index 0000000..a232bee
--- /dev/null
@@ -0,0 +1,200 @@
+<?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/>.
+
+/**
+ * CLI Bulk course registration script from a comma separated file.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2012 Piers Harding
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+
+require(__DIR__ . '/../../../../config.php');
+require_once($CFG->libdir . '/clilib.php');
+require_once($CFG->libdir . '/coursecatlib.php');
+require_once($CFG->libdir . '/csvlib.class.php');
+
+$courseconfig = get_config('moodlecourse');
+
+// Now get cli options.
+list($options, $unrecognized) = cli_get_params(array(
+    'help' => false,
+    'mode' => '',
+    'updatemode' => 'nothing',
+    'file' => '',
+    'delimiter' => 'comma',
+    'encoding' => 'UTF-8',
+    'shortnametemplate' => '',
+    'templatecourse' => false,
+    'restorefile' => false,
+    'allowdeletes' => false,
+    'allowrenames' => false,
+    'allowresets' => false,
+    'reset' => false,
+    'category' => coursecat::get_default()->id,
+),
+array(
+    'h' => 'help',
+    'm' => 'mode',
+    'u' => 'updatemode',
+    'f' => 'file',
+    'd' => 'delimiter',
+    'e' => 'encoding',
+    't' => 'templatecourse',
+    'r' => 'restorefile',
+    'g' => 'format',
+));
+
+if ($unrecognized) {
+    $unrecognized = implode("\n  ", $unrecognized);
+    cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
+}
+
+$help =
+"Execute Course Upload.
+
+Options:
+-h, --help                 Print out this help
+-m, --mode                 Import mode: createnew, createall, createorupdate, update
+-u, --updatemode           Update mode: nothing, dataonly, dataordefaults¸ missingonly
+-f, --file                 CSV file
+-d, --delimiter            CSV delimiter: colon, semicolon, tab, cfg, comma
+-e, --encoding             CSV file encoding: utf8, ... etc
+-t, --templatecourse       Shortname of the course to restore after import
+-r, --restorefile          Backup file to restore after import
+--reset                    Run the course reset after each course import
+--allowdeletes             Allow courses to be deleted
+--allowrenames             Allow courses to be renamed
+--allowresets              Allow courses to be reset
+--shortnametemplate        Template to generate the shortname from
+--category                 ID of default category (--updatemode dataordefaults will use this value)
+
+
+Example:
+\$sudo -u www-data /usr/bin/php admin/tool/uploadcourse/cli/uploadcourse.php --action=createnew \\
+       --updatemode=dataonly --file=./courses.csv --delimiter=comma
+";
+
+if ($options['help']) {
+    echo $help;
+    die();
+}
+echo "Moodle course uploader running ...\n";
+
+$processoroptions = array(
+    'allowdeletes' => $options['allowdeletes'],
+    'allowrenames' => $options['allowrenames'],
+    'allowresets' => $options['allowresets'],
+    'reset' => $options['reset'],
+    'shortnametemplate' => $options['shortnametemplate']
+);
+
+// Confirm that the mode is valid.
+$modes = array(
+    'createnew' => tool_uploadcourse_processor::MODE_CREATE_NEW,
+    'createall' => tool_uploadcourse_processor::MODE_CREATE_ALL,
+    'createorupdate' => tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE,
+    'update' => tool_uploadcourse_processor::MODE_UPDATE_ONLY
+);
+if (!isset($options['mode']) || !isset($modes[$options['mode']])) {
+    echo get_string('invalidmode', 'tool_uploadcourse')."\n";
+    echo $help;
+    die();
+}
+$processoroptions['mode'] = $modes[$options['mode']];
+
+// Check that the update mode is valid.
+$updatemodes = array(
+    'nothing' => tool_uploadcourse_processor::UPDATE_NOTHING,
+    'dataonly' => tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY,
+    'dataordefaults' => tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS,
+    'missingonly' => tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS
+);
+if (($processoroptions['mode'] === tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE ||
+        $processoroptions['mode'] === tool_uploadcourse_processor::MODE_UPDATE_ONLY)
+        && (!isset($options['updatemode']) || !isset($updatemodes[$options['updatemode']]))) {
+    echo get_string('invalideupdatemode', 'tool_uploadcourse')."\n";
+    echo $help;
+    die();
+}
+$processoroptions['updatemode'] = $updatemodes[$options['updatemode']];
+
+// File.
+if (!empty($options['file'])) {
+    $options['file'] = realpath($options['file']);
+}
+if (!file_exists($options['file'])) {
+    echo get_string('invalidcsvfile', 'tool_uploadcourse')."\n";
+    echo $help;
+    die();
+}
+
+// Encoding.
+$encodings = textlib::get_encodings();
+if (!isset($encodings[$options['encoding']])) {
+    echo get_string('invalidencoding', 'tool_uploadcourse')."\n";
+    echo $help;
+    die();
+}
+
+// Default values.
+$defaults = array();
+$defaults['category'] = $options['category'];
+$defaults['startdate'] = time() + 3600 * 24;
+$defaults['newsitems'] = $courseconfig->newsitems;
+$defaults['showgrades'] = $courseconfig->showgrades;
+$defaults['showreports'] = $courseconfig->showreports;
+$defaults['maxbytes'] = $courseconfig->maxbytes;
+$defaults['legacyfiles'] = $CFG->legacyfilesinnewcourses;
+$defaults['groupmode'] = $courseconfig->groupmode;
+$defaults['groupmodeforce'] = $courseconfig->groupmodeforce;
+$defaults['visible'] = $courseconfig->visible;
+$defaults['lang'] =  $courseconfig->lang;
+
+// Course template.
+if (isset($options['templatecourse'])) {
+    $processoroptions['templatecourse'] = $options['templatecourse'];
+}
+
+// Restore file.
+if ($options['restorefile']) {
+    $options['restorefile'] = realpath($options['restorefile']);
+}
+if ($options['restorefile'] && !file_exists($options['restorefile'])) {
+    echo get_string('invalidrestorefile', 'tool_uploadcourse')."\n";
+    echo $help;
+    die();
+}
+$processoroptions['restorefile'] = $options['restorefile'];
+
+// Emulate normal session.
+cron_setup_user();
+
+// Let's get started!
+$content = file_get_contents($options['file']);
+$importid = csv_import_reader::get_new_iid('uploadcourse');
+$cir = new csv_import_reader($importid, 'uploadcourse');
+$readcount = $cir->load_csv_content($content, $options['encoding'], $options['delimiter']);
+unset($content);
+if ($readcount === false) {
+    print_error('csvfileerror', 'tool_uploadcourse', '', $cir->get_error());
+} else if ($readcount == 0) {
+    print_error('csvemptyfile', 'error', '', $cir->get_error());
+}
+$processor = new tool_uploadcourse_processor($cir, $processoroptions, $defaults);
+$processor->execute(new tool_uploadcourse_tracker(tool_uploadcourse_tracker::OUTPUT_PLAIN));
similarity index 79%
rename from enrol/authorize/db/messages.php
rename to admin/tool/uploadcourse/db/caches.php
index 8d3df01..7c0c7c0 100644 (file)
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Defines message providers (types of message sent) for the PayPal enrolment plugin.
+ * Cache definitions.
  *
- * @package    enrol_authorize
- * @copyright  2012 Andrew Davis
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
 defined('MOODLE_INTERNAL') || die();
 
-$messageproviders = array(
-    'authorize_enrolment' => array(),
+$definitions = array(
+    'helper' => array(
+        'mode' => cache_store::MODE_REQUEST,
+    )
 );
diff --git a/admin/tool/uploadcourse/index.php b/admin/tool/uploadcourse/index.php
new file mode 100644 (file)
index 0000000..667551e
--- /dev/null
@@ -0,0 +1,102 @@
+<?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/>.
+
+/**
+ * Bulk course registration script from a comma separated file.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2011 Piers Harding
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require(__DIR__ . '/../../../config.php');
+require_once($CFG->libdir . '/adminlib.php');
+require_once($CFG->libdir . '/coursecatlib.php');
+require_once($CFG->libdir . '/csvlib.class.php');
+
+admin_externalpage_setup('tooluploadcourse');
+
+$importid         = optional_param('importid', '', PARAM_INT);
+$previewrows = optional_param('previewrows', 10, PARAM_INT);
+
+$returnurl = new moodle_url('/admin/tool/uploadcourse/index.php');
+
+if (empty($importid)) {
+    $mform1 = new tool_uploadcourse_step1_form();
+    if ($form1data = $mform1->get_data()) {
+        $importid = csv_import_reader::get_new_iid('uploadcourse');
+        $cir = new csv_import_reader($importid, 'uploadcourse');
+        $content = $mform1->get_file_content('coursefile');
+        $readcount = $cir->load_csv_content($content, $form1data->encoding, $form1data->delimiter_name);
+        unset($content);
+        if ($readcount === false) {
+            print_error('csvfileerror', 'tool_uploadcourse', $returnurl, $cir->get_error());
+        } else if ($readcount == 0) {
+            print_error('csvemptyfile', 'error', $returnurl, $cir->get_error());
+        }
+    } else {
+        echo $OUTPUT->header();
+        echo $OUTPUT->heading_with_help(get_string('uploadcourses', 'tool_uploadcourse'), 'uploadcourses', 'tool_uploadcourse');
+        $mform1->display();
+        echo $OUTPUT->footer();
+        die();
+    }
+} else {
+    $cir = new csv_import_reader($importid, 'uploadcourse');
+}
+
+// Data to set in the form.
+$data = array('importid' => $importid, 'previewrows' => $previewrows);
+if (!empty($form1data)) {
+    // Get options from the first form to pass it onto the second.
+    foreach ($form1data->options as $key => $value) {
+        $data["options[$key]"] = $value;
+    }
+}
+$context = context_system::instance();
+$mform2 = new tool_uploadcourse_step2_form(null, array('contextid' => $context->id, 'columns' => $cir->get_columns(),
+    'data' => $data));
+
+// If a file has been uploaded, then process it.
+if ($form2data = $mform2->is_cancelled()) {
+    $cir->cleanup(true);
+    redirect($returnurl);
+} else if ($form2data = $mform2->get_data()) {
+
+    $options = (array) $form2data->options;
+    $defaults = (array) $form2data->defaults;
+    $processor = new tool_uploadcourse_processor($cir, $options, $defaults);
+
+    echo $OUTPUT->header();
+    if (isset($form2data->showpreview)) {
+        echo $OUTPUT->heading(get_string('uploadcoursespreview', 'tool_uploadcourse'));
+        $processor->preview($previewrows, new tool_uploadcourse_tracker(tool_uploadcourse_tracker::OUTPUT_HTML));
+        $mform2->display();
+    } else {
+        echo $OUTPUT->heading(get_string('uploadcoursesresult', 'tool_uploadcourse'));
+        $processor->execute(new tool_uploadcourse_tracker(tool_uploadcourse_tracker::OUTPUT_HTML));
+        echo $OUTPUT->continue_button($returnurl);
+    }
+
+} else {
+    $processor = new tool_uploadcourse_processor($cir, $form1data->options, array());
+    echo $OUTPUT->header();
+    echo $OUTPUT->heading(get_string('uploadcoursespreview', 'tool_uploadcourse'));
+    $processor->preview($previewrows, new tool_uploadcourse_tracker(tool_uploadcourse_tracker::OUTPUT_HTML));
+    $mform2->display();
+}
+
+echo $OUTPUT->footer();
diff --git a/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php b/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php
new file mode 100644 (file)
index 0000000..83138d3
--- /dev/null
@@ -0,0 +1,115 @@
+<?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/>.
+
+/**
+ * Strings for component 'tool_uploadcourse'.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2011 Piers Harding
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['allowdeletes'] = 'Allow deletes';
+$string['allowrenames'] = 'Allow renames';
+$string['allowresets'] = 'Allow resets';
+$string['cachedef_helper'] = 'Helper caching';
+$string['cannotdeletecoursenotexist'] = 'Cannot delete a course that does not exist';
+$string['cannotgenerateshortnameupdatemode'] = 'Cannot generate a shortname when updates are allowed';
+$string['cannotreadbackupfile'] = 'Cannot read the backup file';
+$string['cannotrenamecoursenotexist'] = 'Cannot rename a course that does not exist';
+$string['cannotrenameidnumberconflict'] = 'Cannot rename the course, the ID number conflicts with an existing course';
+$string['cannotrenameshortnamealreadyinuse'] = 'Cannot rename the course, the shortname is already used';
+$string['canonlyrenameinupdatemode'] = 'Can only rename a course when update is allowed';
+$string['canonlyresetcourseinupdatemode'] = 'Can only reset a course in update mode';
+$string['couldnotresolvecatgorybyid'] = 'Could not resolve category by ID';
+$string['couldnotresolvecatgorybyidnumber'] = 'Could not resolve category by ID number';
+$string['couldnotresolvecatgorybypath'] = 'Could not resolve category by path';
+$string['coursecreated'] = 'Course created';
+$string['coursedeleted'] = 'Course deleted';
+$string['coursedeletionnotallowed'] = 'Course deletion is not allowed';
+$string['coursedoesnotexistandcreatenotallowed'] = 'The course does not exist and creating course is not allowed';
+$string['courseexistsanduploadnotallowed'] = 'The course exists and update is not allowed';
+$string['courseidnumberincremented'] = 'Course ID number incremented {$a->from} -> {$a->to}';
+$string['courseprocess'] = 'Course process';
+$string['courserenamed'] = 'Course renamed';
+$string['courserenamingnotallowed'] = 'Course renaming is not allowed';
+$string['coursereset'] = 'Course reset';
+$string['courseresetnotallowed'] = 'Course reset now allowed';
+$string['courserestored'] = 'Course restored';
+$string['coursestotal'] = 'Courses total: {$a}';
+$string['coursescreated'] = 'Courses created: {$a}';
+$string['coursesupdated'] = 'Courses updated: {$a}';
+$string['coursesdeleted'] = 'Courses deleted: {$a}';
+$string['courseserrors'] = 'Courses errors: {$a}';
+$string['courseshortnameincremented'] = 'Course shortname incremented {$a->from} -> {$a->to}';
+$string['courseshortnamegenerated'] = 'Course shortname generated: {$a}';
+$string['coursetemplatename'] = 'Restore from this course after upload';
+$string['coursetemplatename_help'] = 'Enter an existing course shortname to use as a template for the creation of all courses.';
+$string['coursetorestorefromdoesnotexist'] = 'The course to restore from does not exist';
+$string['courseupdated'] = 'Course updated';
+$string['createall'] = 'Create all, increment shortname if needed';
+$string['createnew'] = 'Create new courses only, skip existing ones';
+$string['createorupdate'] = 'Create new courses, or update existing ones';
+$string['csvdelimiter'] = 'CSV delimiter';
+$string['csvfileerror'] = 'There is something wrong with the format of the CSV file. Please check the number of headings and columns match, and that the delimiter and file encoding are correct: {$a}';
+$string['csvline'] = 'Line';
+$string['defaultvalues'] = 'Default course values';
+$string['encoding'] = 'Encoding';
+$string['errorwhilerestoringcourse'] = 'Error while restoring the course';
+$string['errorwhiledeletingcourse'] = 'Error while deleting the course';
+$string['generatedshortnameinvalid'] = 'The generated shortname is invalid';
+$string['generatedshortnamealreadyinuse'] = 'The generated shortname is already in use';
+$string['id'] = 'ID';
+$string['importoptions'] = 'Import options';
+$string['idnumberalreadyinuse'] = 'ID number already used by a course';
+$string['invalidbackupfile'] = 'Invalid backup file';
+$string['invalidcourseformat'] = 'Invalid course format';
+$string['invalidcsvfile'] = 'Invalid input CSV file';
+$string['invalidencoding'] = 'Invalid encoding';
+$string['invalidmode'] = 'Invalid mode selected';
+$string['invalideupdatemode'] = 'Invalid update mode selected';
+$string['invalidroles'] = 'Invalid role names: {$a}';
+$string['invalidshortname'] = 'Invalid shortname';
+$string['missingmandatoryfields'] = 'Missing value for mandatory fields: {$a}';
+$string['missingshortnamenotemplate'] = 'Missing shortname and shortname template not set';
+$string['mode'] = 'Upload mode';
+$string['nochanges'] = 'No changes';
+$string['pluginname'] = 'Course upload';
+$string['preview'] = 'Preview';
+$string['reset'] = 'Reset course after upload';
+$string['result'] = 'Result';
+$string['restoreafterimport'] = 'Restore after import';
+$string['rowpreviewnum'] = 'Preview rows';
+$string['shortnametemplate'] = 'Template to generate a shortname';
+$string['shortnametemplate_help'] = 'The short name of the course is displayed in the navigation. You may use template syntax here (%f = fullname, %i = idnumber), or enter an initial value that is incremented.';
+$string['templatefile'] = 'Restore from this file after upload';
+$string['templatefile_help'] = 'Select a file to use as a template for the creation of all courses.';
+$string['unknownimportmode'] = 'Unknown import mode';
+$string['updatemissing'] = 'Fill in missing from CSV data and defaults';
+$string['updatemode'] = 'Update mode';
+$string['updatemodedoessettonothing'] = 'Update mode does not allow anything to be updated';
+$string['updateonly'] = 'Only update existing courses';
+$string['updatewithdataordefaults'] = 'Update with CSV data and defaults';
+$string['updatewithdataonly'] = 'Update with CSV data only';
+$string['uploadcourses'] = 'Upload courses';
+$string['uploadcourses_help'] = 'Courses may be uploaded via text file. The format of the file should be as follows:
+
+* Each line of the file contains one record
+* Each record is a series of data separated by commas (or other delimiters)
+* The first record contains a list of fieldnames defining the format of the rest of the file
+* Required fieldnames are shortname, fullname, summary and category';
+$string['uploadcoursespreview'] = 'Upload courses preview';
+$string['uploadcoursesresult'] = 'Upload courses results';
similarity index 59%
rename from enrol/authorize/db/upgrade.php
rename to admin/tool/uploadcourse/settings.php
index adbb27d..9b485d9 100644 (file)
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Authorize.Net enrolment plugin upgrades.
+ * Link to CSV course upload.
  *
- * @package    enrol_authorize
- * @copyright  2006 Eugene Venter
- * @author     Eugene Venter
+ * @package    tool_uploadcourse
+ * @copyright  2011 Piers Harding
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-function xmldb_enrol_authorize_upgrade($oldversion) {
-    global $CFG, $DB;
+defined('MOODLE_INTERNAL') || die();
 
-    $dbman = $DB->get_manager();
-
-
-    // Moodle v2.3.0 release upgrade line
-    // Put any upgrade step following this
-
-
-    // Moodle v2.4.0 release upgrade line
-    // Put any upgrade step following this
-
-
-    // Moodle v2.5.0 release upgrade line.
-    // Put any upgrade step following this.
-
-
-    return true;
+if ($hassiteconfig) {
+    $ADMIN->add('courses', new admin_externalpage('tooluploadcourse',
+        get_string('uploadcourses', 'tool_uploadcourse'), "/admin/tool/uploadcourse/index.php"));
 }
diff --git a/admin/tool/uploadcourse/tests/behat/create.feature b/admin/tool/uploadcourse/tests/behat/create.feature
new file mode 100644 (file)
index 0000000..ee95ca1
--- /dev/null
@@ -0,0 +1,46 @@
+@tool @tool_uploadcourse @_only_local
+Feature: An admin can create courses using a CSV file
+  In order to create courses using a CSV file
+  As an admin
+  I need to be able to upload a CSV file and navigate through the import process
+
+  Background:
+    Given the following "courses" exists:
+      | fullname | shortname | category |
+      | First course | C1 | 0 |
+    And I log in as "admin"
+    And I expand "Site administration" node
+    And I expand "Courses" node
+    And I follow "Upload courses"
+
+  @javascript
+  Scenario: Creation of unexisting courses
+    Given I upload "admin/tool/uploadcourse/tests/fixtures/courses.csv" file to "File" filepicker
+    And I click on "Preview" "button"
+    When I click on "Upload courses" "button"
+    Then I should see "The course exists and update is not allowed"
+    And I should see "Course created"
+    And I should see "Courses total: 3"
+    And I should see "Courses created: 2"
+    And I should see "Courses errors: 1"
+    And I follow "Home"
+    And I should see "Course 2"
+    And I should see "Course 3"
+
+  @javascript
+  Scenario: Creation of existing courses
+    Given I upload "admin/tool/uploadcourse/tests/fixtures/courses.csv" file to "File" filepicker
+    And I select "Create all, increment shortname if needed" from "Upload mode"
+    And I click on "Preview" "button"
+    When I click on "Upload courses" "button"
+    Then I should see "Course created"
+    And I should see "Course shortname incremented C1 -> C2"
+    And I should see "Course shortname incremented C2 -> C3"
+    And I should see "Course shortname incremented C3 -> C4"
+    And I should see "Courses total: 3"
+    And I should see "Courses created: 3"
+    And I should see "Courses errors: 0"
+    And I follow "Home"
+    And I should see "Course 1"
+    And I should see "Course 2"
+    And I should see "Course 3"
diff --git a/admin/tool/uploadcourse/tests/behat/update.feature b/admin/tool/uploadcourse/tests/behat/update.feature
new file mode 100644 (file)
index 0000000..3a06d16
--- /dev/null
@@ -0,0 +1,32 @@
+@tool @tool_uploadcourse @_only_local
+Feature: An admin can update courses using a CSV file
+  In order to update courses using a CSV file
+  As an admin
+  I need to be able to upload a CSV file and navigate through the import process
+
+  Background:
+    Given the following "courses" exists:
+      | fullname | shortname | category |
+      | Some random name | C1 | 0 |
+    And I log in as "admin"
+    And I expand "Site administration" node
+    And I expand "Courses" node
+    And I follow "Upload courses"
+
+  @javascript
+  Scenario: Updating a course fullname
+    Given I upload "admin/tool/uploadcourse/tests/fixtures/courses.csv" file to "File" filepicker
+    And I select "Only update existing courses" from "Upload mode"
+    And I select "Update with CSV data only" from "Update mode"
+    And I click on "Preview" "button"
+    When I click on "Upload courses" "button"
+    Then I should see "Course updated"
+    And I should see "The course does not exist and creating course is not allowed"
+    And I should see "Courses total: 3"
+    And I should see "Courses updated: 1"
+    And I should see "Courses created: 0"
+    And I should see "Courses errors: 2"
+    And I follow "Home"
+    And I should see "Course 1"
+    And I should not see "Course 2"
+    And I should not see "Course 3"
diff --git a/admin/tool/uploadcourse/tests/course_test.php b/admin/tool/uploadcourse/tests/course_test.php
new file mode 100644 (file)
index 0000000..f04a2ea
--- /dev/null
@@ -0,0 +1,923 @@
+<?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/>.
+
+/**
+ * File containing tests for the course class.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+/**
+ * Course test case.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or late
+ */
+class tool_uploadcourse_course_testcase extends advanced_testcase {
+
+    public function test_proceed_without_prepare() {
+        $this->resetAfterTest(true);
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+        $data = array();
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->setExpectedException('coding_exception');
+        $co->proceed();
+    }
+
+    public function test_proceed_when_prepare_failed() {
+        $this->resetAfterTest(true);
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+        $data = array();
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertFalse($co->prepare());
+        $this->setExpectedException('moodle_exception');
+        $co->proceed();
+    }
+
+    public function test_proceed_when_already_started() {
+        $this->resetAfterTest(true);
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+        $data = array('shortname' => 'test', 'fullname' => 'New course', 'summary' => 'New', 'category' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->setExpectedException('coding_exception');
+        $co->proceed();
+    }
+
+    public function test_invalid_shortname() {
+        $this->resetAfterTest(true);
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+        $data = array('shortname' => '<invalid>', 'fullname' => 'New course', 'summary' => 'New', 'category' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('invalidshortname', $co->get_errors());
+    }
+
+    public function test_create() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        // Existing course.
+        $c1 = $this->getDataGenerator()->create_course(array('shortname' => 'c1', 'summary' => 'Yay!'));
+        $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1')));
+
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+
+        // Try to add a new course.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $data = array('shortname' => 'newcourse', 'fullname' => 'New course', 'summary' => 'New', 'category' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $this->assertFalse($DB->record_exists('course', array('shortname' => 'newcourse')));
+        $co->proceed();
+        $this->assertTrue($DB->record_exists('course', array('shortname' => 'newcourse')));
+
+        // Try to add a new course, that already exists.
+        $coursecount = $DB->count_records('course', array());
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $data = array('shortname' => 'c1', 'fullname' => 'C1FN', 'summary' => 'C1', 'category' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('courseexistsanduploadnotallowed', $co->get_errors());
+        $this->assertEquals($coursecount, $DB->count_records('course', array()));
+        $this->assertNotEquals('C1', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1')));
+
+        // Try to add new with shortname incrementation.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_ALL;
+        $data = array('shortname' => 'c1', 'fullname' => 'C1FN', 'summary' => 'C1', 'category' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertTrue($DB->record_exists('course', array('shortname' => 'c2')));
+    }
+
+    public function test_delete() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $c1 = $this->getDataGenerator()->create_course();
+        $c2 = $this->getDataGenerator()->create_course();
+
+        $this->assertTrue($DB->record_exists('course', array('shortname' => $c1->shortname)));
+        $this->assertFalse($DB->record_exists('course', array('shortname' => 'DoesNotExist')));
+
+        $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+
+        // Try delete when option not available.
+        $importoptions = array('candelete' => false);
+        $data = array('shortname' => $c1->shortname, 'delete' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('coursedeletionnotallowed', $co->get_errors());
+        $this->assertTrue($DB->record_exists('course', array('shortname' => $c1->shortname)));
+
+        // Try delete when not requested.
+        $importoptions = array('candelete' => true);
+        $data = array('shortname' => $c1->shortname, 'delete' => 0);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertTrue($DB->record_exists('course', array('shortname' => $c1->shortname)));
+
+        // Try delete when requested.
+        $importoptions = array('candelete' => true);
+        $data = array('shortname' => $c1->shortname, 'delete' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertFalse($DB->record_exists('course', array('shortname' => $c1->shortname)));
+        $this->assertTrue($DB->record_exists('course', array('shortname' => $c2->shortname)));
+
+        // Try deleting non-existing record, this should not fail.
+        $data = array('shortname' => 'DoesNotExist', 'delete' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('cannotdeletecoursenotexist', $co->get_errors());
+    }
+
+    public function test_update() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $c1 = $this->getDataGenerator()->create_course(array('shortname' => 'c1'));
+
+        // Try to update with existing shortnames, not allowing creation, and updating nothing.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+        $data = array('shortname' => 'c1', 'fullname' => 'New fullname');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('updatemodedoessettonothing', $co->get_errors());
+
+        // Try to update with non-existing shortnames.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => 'DoesNotExist', 'fullname' => 'New fullname');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('coursedoesnotexistandcreatenotallowed', $co->get_errors());
+
+        // Try a proper update.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => 'c1', 'fullname' => 'New fullname');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertEquals('New fullname', $DB->get_field_select('course', 'fullname', 'shortname = :s', array('s' => 'c1')));
+
+        // Try a proper update with defaults.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS;
+        $data = array('shortname' => 'c1', 'fullname' => 'Another fullname');
+        $defaults = array('fullname' => 'Not this one', 'summary' => 'Awesome summary');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaults);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertEquals('Another fullname', $DB->get_field_select('course', 'fullname', 'shortname = :s', array('s' => 'c1')));
+        $this->assertEquals('Awesome summary', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1')));
+
+        // Try a proper update missing only.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS;
+        $DB->set_field('course', 'summary', '', array('shortname' => 'c1'));
+        $this->assertEquals('', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1')));
+        $data = array('shortname' => 'c1', 'summary' => 'Fill in summary');
+        $defaults = array('summary' => 'Do not use this summary');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaults);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertEquals('Fill in summary', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1')));
+
+        // Try a proper update missing only using defaults.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS;
+        $DB->set_field('course', 'summary', '', array('shortname' => 'c1'));
+        $this->assertEquals('', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1')));
+        $data = array('shortname' => 'c1');
+        $defaults = array('summary' => 'Use this summary');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaults);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertEquals('Use this summary', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1')));
+    }
+
+    public function test_data_saved() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        // Create.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+        $data = array(
+            'shortname' => 'c1',
+            'fullname' => 'Fullname',
+            'category' => '1',
+            'visible' => '0',
+            'startdate' => '8 June 1990',
+            'idnumber' => '123abc',
+            'summary' => 'Summary',
+            'format' => 'weeks',
+            'theme' => 'afterburner',
+            'lang' => 'en',
+            'newsitems' => '7',
+            'showgrades' => '0',
+            'showreports' => '1',
+            'legacyfiles' => '1',
+            'maxbytes' => '1234',
+            'groupmode' => '2',
+            'groupmodeforce' => '1',
+            'enablecompletion' => '1',
+
+            'role_teacher' => 'Knight',
+            'role_manager' => 'Jedi',
+
+            'enrolment_1' => 'guest',
+            'enrolment_2' => 'self',
+            'enrolment_2_roleid' => '1',
+            'enrolment_3' => 'manual',
+            'enrolment_3_disable' => '1',
+        );
+
+        $this->assertFalse($DB->record_exists('course', array('shortname' => 'c1')));
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1')));
+        $course = $DB->get_record('course', array('shortname' => 'c1'));
+        $ctx = context_course::instance($course->id);
+
+        $this->assertEquals($data['fullname'], $course->fullname);
+        $this->assertEquals($data['category'], $course->category);
+        $this->assertEquals($data['visible'], $course->visible);
+        $this->assertEquals(mktime(0, 0, 0, 6, 8, 1990), $course->startdate);
+        $this->assertEquals($data['idnumber'], $course->idnumber);
+        $this->assertEquals($data['summary'], $course->summary);
+        $this->assertEquals($data['format'], $course->format);
+        $this->assertEquals($data['theme'], $course->theme);
+        $this->assertEquals($data['lang'], $course->lang);
+        $this->assertEquals($data['newsitems'], $course->newsitems);
+        $this->assertEquals($data['showgrades'], $course->showgrades);
+        $this->assertEquals($data['showreports'], $course->showreports);
+        $this->assertEquals($data['legacyfiles'], $course->legacyfiles);
+        $this->assertEquals($data['maxbytes'], $course->maxbytes);
+        $this->assertEquals($data['groupmode'], $course->groupmode);
+        $this->assertEquals($data['groupmodeforce'], $course->groupmodeforce);
+        $this->assertEquals($data['enablecompletion'], $course->enablecompletion);
+
+        // Roles.
+        $roleids = array();
+        $roles = get_all_roles();
+        foreach ($roles as $role) {
+            $roleids[$role->shortname] = $role->id;
+        }
+        $this->assertEquals('Knight', $DB->get_field_select('role_names', 'name',
+            'roleid = :roleid AND contextid = :ctxid', array('ctxid' => $ctx->id, 'roleid' => $roleids['teacher'])));
+        $this->assertEquals('Jedi', $DB->get_field_select('role_names', 'name',
+            'roleid = :roleid AND contextid = :ctxid', array('ctxid' => $ctx->id, 'roleid' => $roleids['manager'])));
+
+        // Enrolment methods.
+        $enroldata = array();
+        $instances = enrol_get_instances($course->id, false);
+        $this->assertCount(3, $instances);
+        foreach ($instances as $instance) {
+            $enroldata[$instance->enrol] = $instance;
+        }
+
+        $this->assertNotEmpty($enroldata['guest']);
+        $this->assertEquals(ENROL_INSTANCE_ENABLED, $enroldata['guest']->status);
+        $this->assertNotEmpty($enroldata['self']);
+        $this->assertEquals($data['enrolment_2_roleid'], $enroldata['self']->roleid);
+        $this->assertEquals(ENROL_INSTANCE_ENABLED, $enroldata['self']->status);
+        $this->assertNotEmpty($enroldata['manual']);
+        $this->assertEquals(ENROL_INSTANCE_DISABLED, $enroldata['manual']->status);
+
+        // Update existing course.
+        $cat = $this->getDataGenerator()->create_category();
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array(
+            'shortname' => 'c1',
+            'fullname' => 'Fullname 2',
+            'category' => $cat->id,
+            'visible' => '1',
+            'startdate' => '11 June 1984',
+            'idnumber' => 'changeidn',
+            'summary' => 'Summary 2',
+            'format' => 'topics',
+            'theme' => 'clean',
+            'lang' => '',
+            'newsitems' => '2',
+            'showgrades' => '1',
+            'showreports' => '0',
+            'legacyfiles' => '0',
+            'maxbytes' => '4321',
+            'groupmode' => '1',
+            'groupmodeforce' => '0',
+            'enablecompletion' => '0',
+
+            'role_teacher' => 'Teacher',
+            'role_manager' => 'Manager',
+
+            'enrolment_1' => 'guest',
+            'enrolment_1_disable' => '1',
+            'enrolment_2' => 'self',
+            'enrolment_2_roleid' => '2',
+            'enrolment_3' => 'manual',
+            'enrolment_3_delete' => '1',
+        );
+
+        $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1')));
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $course = $DB->get_record('course', array('shortname' => 'c1'));
+        $ctx = context_course::instance($course->id);
+
+        $this->assertEquals($data['fullname'], $course->fullname);
+        $this->assertEquals($data['category'], $course->category);
+        $this->assertEquals($data['visible'], $course->visible);
+        $this->assertEquals(mktime(0, 0, 0, 6, 11, 1984), $course->startdate);
+        $this->assertEquals($data['idnumber'], $course->idnumber);
+        $this->assertEquals($data['summary'], $course->summary);
+        $this->assertEquals($data['format'], $course->format);
+        $this->assertEquals($data['theme'], $course->theme);
+        $this->assertEquals($data['lang'], $course->lang);
+        $this->assertEquals($data['newsitems'], $course->newsitems);
+        $this->assertEquals($data['showgrades'], $course->showgrades);
+        $this->assertEquals($data['showreports'], $course->showreports);
+        $this->assertEquals($data['legacyfiles'], $course->legacyfiles);
+        $this->assertEquals($data['maxbytes'], $course->maxbytes);
+        $this->assertEquals($data['groupmode'], $course->groupmode);
+        $this->assertEquals($data['groupmodeforce'], $course->groupmodeforce);
+        $this->assertEquals($data['enablecompletion'], $course->enablecompletion);
+
+        // Roles.
+        $roleids = array();
+        $roles = get_all_roles();
+        foreach ($roles as $role) {
+            $roleids[$role->shortname] = $role->id;
+        }
+        $this->assertEquals('Teacher', $DB->get_field_select('role_names', 'name',
+            'roleid = :roleid AND contextid = :ctxid', array('ctxid' => $ctx->id, 'roleid' => $roleids['teacher'])));
+        $this->assertEquals('Manager', $DB->get_field_select('role_names', 'name',
+            'roleid = :roleid AND contextid = :ctxid', array('ctxid' => $ctx->id, 'roleid' => $roleids['manager'])));
+
+        // Enrolment methods.
+        $enroldata = array();
+        $instances = enrol_get_instances($course->id, false);
+        $this->assertCount(2, $instances);
+        foreach ($instances as $instance) {
+            $enroldata[$instance->enrol] = $instance;
+        }
+
+        $this->assertNotEmpty($enroldata['guest']);
+        $this->assertEquals(ENROL_INSTANCE_DISABLED, $enroldata['guest']->status);
+        $this->assertNotEmpty($enroldata['self']);
+        $this->assertEquals($data['enrolment_2_roleid'], $enroldata['self']->roleid);
+        $this->assertEquals(ENROL_INSTANCE_ENABLED, $enroldata['self']->status);
+    }
+
+    public function test_default_data_saved() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        // Create.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+        $data = array(
+            'shortname' => 'c1',
+        );
+        $defaultdata = array(
+            'fullname' => 'Fullname',
+            'category' => '1',
+            'visible' => '0',
+            'startdate' => '8 June 1990',
+            'idnumber' => '123abc',
+            'summary' => 'Summary',
+            'format' => 'weeks',
+            'theme' => 'afterburner',
+            'lang' => 'en',
+            'newsitems' => '7',
+            'showgrades' => '0',
+            'showreports' => '1',
+            'legacyfiles' => '1',
+            'maxbytes' => '1234',
+            'groupmode' => '2',
+            'groupmodeforce' => '1',
+            'enablecompletion' => '1',
+        );
+
+        $this->assertFalse($DB->record_exists('course', array('shortname' => 'c1')));
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaultdata);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1')));
+        $course = $DB->get_record('course', array('shortname' => 'c1'));
+        $ctx = context_course::instance($course->id);
+
+        $this->assertEquals($defaultdata['fullname'], $course->fullname);
+        $this->assertEquals($defaultdata['category'], $course->category);
+        $this->assertEquals($defaultdata['visible'], $course->visible);
+        $this->assertEquals(mktime(0, 0, 0, 6, 8, 1990), $course->startdate);
+        $this->assertEquals($defaultdata['idnumber'], $course->idnumber);
+        $this->assertEquals($defaultdata['summary'], $course->summary);
+        $this->assertEquals($defaultdata['format'], $course->format);
+        $this->assertEquals($defaultdata['theme'], $course->theme);
+        $this->assertEquals($defaultdata['lang'], $course->lang);
+        $this->assertEquals($defaultdata['newsitems'], $course->newsitems);
+        $this->assertEquals($defaultdata['showgrades'], $course->showgrades);
+        $this->assertEquals($defaultdata['showreports'], $course->showreports);
+        $this->assertEquals($defaultdata['legacyfiles'], $course->legacyfiles);
+        $this->assertEquals($defaultdata['maxbytes'], $course->maxbytes);
+        $this->assertEquals($defaultdata['groupmode'], $course->groupmode);
+        $this->assertEquals($defaultdata['groupmodeforce'], $course->groupmodeforce);
+        $this->assertEquals($defaultdata['enablecompletion'], $course->enablecompletion);
+
+        // Update.
+        $cat = $this->getDataGenerator()->create_category();
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS;
+        $data = array(
+            'shortname' => 'c1',
+        );
+        $defaultdata = array(
+            'fullname' => 'Fullname 2',
+            'category' => $cat->id,
+            'visible' => '1',
+            'startdate' => '11 June 1984',
+            'idnumber' => 'changedid',
+            'summary' => 'Summary 2',
+            'format' => 'topics',
+            'theme' => 'clean',
+            'lang' => '',
+            'newsitems' => '2',
+            'showgrades' => '1',
+            'showreports' => '0',
+            'legacyfiles' => '0',
+            'maxbytes' => '1111',
+            'groupmode' => '1',
+            'groupmodeforce' => '0',
+            'enablecompletion' => '0',
+        );
+
+        $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1')));
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaultdata);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1')));
+        $course = $DB->get_record('course', array('shortname' => 'c1'));
+        $ctx = context_course::instance($course->id);
+
+        $this->assertEquals($defaultdata['fullname'], $course->fullname);
+        $this->assertEquals($defaultdata['category'], $course->category);
+        $this->assertEquals($defaultdata['visible'], $course->visible);
+        $this->assertEquals(mktime(0, 0, 0, 6, 11, 1984), $course->startdate);
+        $this->assertEquals($defaultdata['idnumber'], $course->idnumber);
+        $this->assertEquals($defaultdata['summary'], $course->summary);
+        $this->assertEquals($defaultdata['format'], $course->format);
+        $this->assertEquals($defaultdata['theme'], $course->theme);
+        $this->assertEquals($defaultdata['lang'], $course->lang);
+        $this->assertEquals($defaultdata['newsitems'], $course->newsitems);
+        $this->assertEquals($defaultdata['showgrades'], $course->showgrades);
+        $this->assertEquals($defaultdata['showreports'], $course->showreports);
+        $this->assertEquals($defaultdata['legacyfiles'], $course->legacyfiles);
+        $this->assertEquals($defaultdata['maxbytes'], $course->maxbytes);
+        $this->assertEquals($defaultdata['groupmode'], $course->groupmode);
+        $this->assertEquals($defaultdata['groupmodeforce'], $course->groupmodeforce);
+        $this->assertEquals($defaultdata['enablecompletion'], $course->enablecompletion);
+    }
+
+    public function test_rename() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $c1 = $this->getDataGenerator()->create_course(array('shortname' => 'c1'));
+        $c2 = $this->getDataGenerator()->create_course(array('shortname' => 'c2'));
+
+        // Cannot rename when creating.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $importoptions = array('canrename' => true);
+        $data = array('shortname' => 'c1', 'rename' => 'newshortname');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('courseexistsanduploadnotallowed', $co->get_errors());
+
+        // Cannot rename when creating.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_ALL;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $importoptions = array('canrename' => true);
+        $data = array('shortname' => 'c1', 'rename' => 'newshortname', 'category' => 1, 'summary' => 'S', 'fullname' => 'F');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('canonlyrenameinupdatemode', $co->get_errors());
+
+        // Error when not allowed to rename the course.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $importoptions = array('canrename' => false);
+        $data = array('shortname' => 'c1', 'rename' => 'newshortname');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('courserenamingnotallowed', $co->get_errors());
+
+        // Can rename when updating.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $importoptions = array('canrename' => true);
+        $data = array('shortname' => 'c1', 'rename' => 'newshortname');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertEquals('newshortname', $DB->get_field_select('course', 'shortname', 'id = :id', array('id' => $c1->id)));
+
+        // Can rename when updating.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $importoptions = array('canrename' => true);
+        $data = array('shortname' => 'newshortname', 'rename' => 'newshortname2');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertEquals('newshortname2', $DB->get_field_select('course', 'shortname', 'id = :id', array('id' => $c1->id)));
+
+        // Error when course does not exist.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $importoptions = array('canrename' => true);
+        $data = array('shortname' => 'DoesNotExist', 'rename' => 'c1', 'category' => 1, 'summary' => 'S', 'fullname' => 'F');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('cannotrenamecoursenotexist', $co->get_errors());
+
+        // Renaming still updates the other values.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS;
+        $importoptions = array('canrename' => true);
+        $data = array('shortname' => 'newshortname2', 'rename' => 'c1', 'fullname' => 'Another fullname!');
+        $defaultdata = array('summary' => 'New summary!');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaultdata, $importoptions);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertEquals('c1', $DB->get_field_select('course', 'shortname', 'id = :id', array('id' => $c1->id)));
+        $this->assertEquals('New summary!', $DB->get_field_select('course', 'summary', 'id = :id', array('id' => $c1->id)));
+        $this->assertEquals('Another fullname!', $DB->get_field_select('course', 'fullname', 'id = :id', array('id' => $c1->id)));
+
+        // Renaming with invalid shortname.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $importoptions = array('canrename' => true);
+        $data = array('shortname' => 'c1', 'rename' => '<span>invalid</span>');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('invalidshortname', $co->get_errors());
+
+        // Renaming with invalid shortname.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $importoptions = array('canrename' => true);
+        $data = array('shortname' => 'c1', 'rename' => 'c2');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('cannotrenameshortnamealreadyinuse', $co->get_errors());
+    }
+
+    public function test_restore_course() {
+        global $DB;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $c1 = $this->getDataGenerator()->create_course();
+        $c1f1 = $this->getDataGenerator()->create_module('forum', array('course' => $c1->id));
+
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => 'A1', 'templatecourse' => $c1->shortname, 'summary' => 'A', 'category' => 1,
+            'fullname' => 'A1');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $course = $DB->get_record('course', array('shortname' => 'A1'));
+        $modinfo = get_fast_modinfo($course);
+        $found = false;
+        foreach ($modinfo->get_cms() as $cmid => $cm) {
+            if ($cm->modname == 'forum' && $cm->name == $c1f1->name) {
+                $found = true;
+                break;
+            }
+        }
+        $this->assertTrue($found);
+
+        // Restore the time limit to prevent warning.
+        set_time_limit(0);
+    }
+
+    public function test_restore_file() {
+        global $DB;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $c1 = $this->getDataGenerator()->create_course();
+        $c1f1 = $this->getDataGenerator()->create_module('forum', array('course' => $c1->id));
+
+        // Restore from a file, checking that the file takes priority over the templatecourse.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => 'A1', 'backupfile' => __DIR__ . '/fixtures/backup.mbz',
+            'summary' => 'A', 'category' => 1, 'fullname' => 'A1', 'templatecourse' => $c1->shortname);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $course = $DB->get_record('course', array('shortname' => 'A1'));
+        $modinfo = get_fast_modinfo($course);
+        $found = false;
+        foreach ($modinfo->get_cms() as $cmid => $cm) {
+            if ($cm->modname == 'glossary' && $cm->name == 'Imported Glossary') {
+                $found = true;
+            } else if ($cm->modname == 'forum' && $cm->name == $c1f1->name) {
+                // We should not find this!
+                $this->assertTrue(false);
+            }
+        }
+        $this->assertTrue($found);
+
+        // Restore the time limit to prevent warning.
+        set_time_limit(0);
+    }
+
+    /**
+     * Testing the reset on groups, group members and enrolments.
+     */
+    public function test_reset() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $c1 = $this->getDataGenerator()->create_course();
+        $c1ctx = context_course::instance($c1->id);
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
+
+        $u1 = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($u1->id, $c1->id, $studentrole->id);
+        $this->assertCount(1, get_enrolled_users($c1ctx));
+
+        $g1 = $this->getDataGenerator()->create_group(array('courseid' => $c1->id));
+        $this->getDataGenerator()->create_group_member(array('groupid' => $g1->id, 'userid' => $u1->id));
+        $this->assertEquals(1, $DB->count_records('groups', array('courseid' => $c1->id)));
+        $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $g1->id, 'userid' => $u1->id)));
+
+        // Wrong mode.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => 'DoesNotExist', 'reset' => '1', 'summary' => 'summary', 'fullname' => 'FN', 'category' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('canonlyresetcourseinupdatemode', $co->get_errors());
+        $this->assertTrue($DB->record_exists('groups', array('id' => $g1->id)));
+        $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $g1->id, 'userid' => $u1->id)));
+        $this->assertCount(1, get_enrolled_users($c1ctx));
+
+        // Reset not allowed.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => $c1->shortname, 'reset' => '1');
+        $importoptions = array('canreset' => false);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('courseresetnotallowed', $co->get_errors());
+        $this->assertTrue($DB->record_exists('groups', array('id' => $g1->id)));
+        $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $g1->id, 'userid' => $u1->id)));
+        $this->assertCount(1, get_enrolled_users($c1ctx));
+
+        // Reset allowed but not requested.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => $c1->shortname, 'reset' => '0');
+        $importoptions = array('canreset' => true);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertTrue($DB->record_exists('groups', array('id' => $g1->id)));
+        $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $g1->id, 'userid' => $u1->id)));
+        $this->assertCount(1, get_enrolled_users($c1ctx));
+
+        // Reset passed as a default parameter, should not be taken in account.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => $c1->shortname);
+        $importoptions = array('canreset' => true);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array('reset' => 1), $importoptions);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertTrue($DB->record_exists('groups', array('id' => $g1->id)));
+        $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $g1->id, 'userid' => $u1->id)));
+        $this->assertCount(1, get_enrolled_users($c1ctx));
+
+        // Reset executed from data.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => $c1->shortname, 'reset' => 1);
+        $importoptions = array('canreset' => true);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertFalse($DB->record_exists('groups', array('id' => $g1->id)));
+        $this->assertFalse($DB->record_exists('groups_members', array('groupid' => $g1->id, 'userid' => $u1->id)));
+        $this->assertCount(0, get_enrolled_users($c1ctx));
+
+        // Reset executed from import option.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => $c1->shortname, 'reset' => 0);
+        $importoptions = array('reset' => 1, 'canreset' => true);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+
+        $g1 = $this->getDataGenerator()->create_group(array('courseid' => $c1->id));
+        $this->getDataGenerator()->create_group_member(array('groupid' => $g1->id, 'userid' => $u1->id));
+        $this->assertEquals(1, $DB->count_records('groups', array('courseid' => $c1->id)));
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertFalse($DB->record_exists('groups', array('id' => $g1->id)));
+    }
+
+    public function test_create_bad_category() {
+        $this->resetAfterTest(true);
+
+        // Ensure fails when category cannot be resolved upon creation.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => 'c1', 'summary' => 'summary', 'fullname' => 'FN', 'category' => 'Wrong cat');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('couldnotresolvecatgorybyid', $co->get_errors());
+
+        // Ensure fails when category cannot be resolved upon update.
+        $c1 = $this->getDataGenerator()->create_course();
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => $c1->shortname, 'category' => 'Wrong cat');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('couldnotresolvecatgorybyid', $co->get_errors());
+    }
+
+    public function test_enrolment_data() {
+        $this->resetAfterTest(true);
+
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => 'c1', 'summary' => 'S', 'fullname' => 'FN', 'category' => '1');
+        $data['enrolment_1'] = 'manual';
+        $data['enrolment_1_role'] = 'teacher';
+        $data['enrolment_1_startdate'] = '2nd July 2013';
+        $data['enrolment_1_enddate'] = '2nd August 2013';
+        $data['enrolment_1_enrolperiod'] = '10 days';
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+
+        // Enrolment methods.
+        $enroldata = array();
+        $instances = enrol_get_instances($co->get_id(), false);
+        foreach ($instances as $instance) {
+            $enroldata[$instance->enrol] = $instance;
+        }
+
+        $this->assertNotEmpty($enroldata['manual']);
+        $this->assertEquals(ENROL_INSTANCE_ENABLED, $enroldata['manual']->status);
+        $this->assertEquals(strtotime($data['enrolment_1_startdate']), $enroldata['manual']->enrolstartdate);
+        $this->assertEquals(strtotime('1970-01-01 GMT + ' . $data['enrolment_1_enrolperiod']), $enroldata['manual']->enrolperiod);
+        $this->assertEquals(strtotime('12th July 2013'), $enroldata['manual']->enrolenddate);
+    }
+
+    public function test_idnumber_problems() {
+        $this->resetAfterTest(true);
+
+        $c1 = $this->getDataGenerator()->create_course(array('shortname' => 'sntaken', 'idnumber' => 'taken'));
+        $c2 = $this->getDataGenerator()->create_course();
+
+        // Create with existing ID number.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => 'c2', 'summary' => 'summary', 'fullname' => 'FN', 'category' => '1',
+            'idnumber' => $c1->idnumber);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('idnumberalreadyinuse', $co->get_errors());
+
+        // Rename to existing ID number.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => $c2->shortname, 'rename' => 'SN', 'idnumber' => $c1->idnumber);
+        $importoptions = array('canrename' => true);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('cannotrenameidnumberconflict', $co->get_errors());
+
+        // Incrementing shortname increments idnumber.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_ALL;
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+        $data = array('shortname' => $c1->shortname, 'idnumber' => $c1->idnumber, 'summary' => 'S', 'fullname' => 'F',
+            'category' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), array());
+        $this->assertTrue($co->prepare());
+        $this->assertArrayHasKey('courseshortnameincremented', $co->get_statuses());
+        $this->assertArrayHasKey('courseidnumberincremented', $co->get_statuses());
+        $data = $co->get_data();
+        $this->assertEquals('sntaken_2', $data['shortname']);
+        $this->assertEquals('taken_2', $data['idnumber']);
+
+        // Incrementing shortname increments idnumber unless available.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_ALL;
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+        $data = array('shortname' => $c1->shortname, 'idnumber' => 'nottaken', 'summary' => 'S', 'fullname' => 'F',
+            'category' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), array());
+        $this->assertTrue($co->prepare());
+        $this->assertArrayHasKey('courseshortnameincremented', $co->get_statuses());
+        $this->assertArrayNotHasKey('courseidnumberincremented', $co->get_statuses());
+        $data = $co->get_data();
+        $this->assertEquals('sntaken_2', $data['shortname']);
+        $this->assertEquals('nottaken', $data['idnumber']);
+    }
+
+    public function test_generate_shortname() {
+        $this->resetAfterTest(true);
+
+        $c1 = $this->getDataGenerator()->create_course(array('shortname' => 'taken'));
+
+        // Generate a shortname.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+        $data = array('summary' => 'summary', 'fullname' => 'FN', 'category' => '1', 'idnumber' => 'IDN');
+        $importoptions = array('shortnametemplate' => '%i');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertTrue($co->prepare());
+        $this->assertArrayHasKey('courseshortnamegenerated', $co->get_statuses());
+
+        // Generate a shortname without a template.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+        $data = array('summary' => 'summary', 'fullname' => 'FN', 'category' => '1');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), array());
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('missingshortnamenotemplate', $co->get_errors());
+
+        // Generate a shortname in update mode.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('summary' => 'summary', 'fullname' => 'FN', 'category' => '1');
+        $importoptions = array('shortnametemplate' => '%f');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        // Commented because we never get here as the course without shortname does not exist.
+        // $this->assertArrayHasKey('cannotgenerateshortnameupdatemode', $co->get_errors());
+
+        // Generate a shortname to a course that already exists.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+        $data = array('summary' => 'summary', 'fullname' => 'taken', 'category' => '1');
+        $importoptions = array('shortnametemplate' => '%f');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('generatedshortnamealreadyinuse', $co->get_errors());
+
+        // Generate a shortname to a course that already exists will be incremented.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_ALL;
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+        $data = array('summary' => 'summary', 'fullname' => 'taken', 'category' => '1');
+        $importoptions = array('shortnametemplate' => '%f');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertTrue($co->prepare());
+        $this->assertArrayHasKey('courseshortnamegenerated', $co->get_statuses());
+        $this->assertArrayHasKey('courseshortnameincremented', $co->get_statuses());
+    }
+
+}
diff --git a/admin/tool/uploadcourse/tests/fixtures/backup.mbz b/admin/tool/uploadcourse/tests/fixtures/backup.mbz
new file mode 100644 (file)
index 0000000..25da174
Binary files /dev/null and b/admin/tool/uploadcourse/tests/fixtures/backup.mbz differ
diff --git a/admin/tool/uploadcourse/tests/fixtures/courses.csv b/admin/tool/uploadcourse/tests/fixtures/courses.csv
new file mode 100644 (file)
index 0000000..2daebe0
--- /dev/null
@@ -0,0 +1,4 @@
+shortname,fullname,summary,category,idnumber
+C1,Course 1,Summary 1,1,ID1
+C2,Course 2,Summary 2,1,ID2
+C3,Course 3,Summary 3,1,ID3
\ No newline at end of file
diff --git a/admin/tool/uploadcourse/tests/helper_test.php b/admin/tool/uploadcourse/tests/helper_test.php
new file mode 100644 (file)
index 0000000..f81c59f
--- /dev/null
@@ -0,0 +1,389 @@
+<?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/>.
+
+/**
+ * File containing tests for the helper.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+/**
+ * Helper test case.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_uploadcourse_helper_testcase extends advanced_testcase {
+
+    public function test_generate_shortname() {
+        $data = (object) array('fullname' => 'Ah bh Ch 01 02 03', 'idnumber' => 'ID123');
+
+        $this->assertSame($data->fullname, tool_uploadcourse_helper::generate_shortname($data, '%f'));
+        $this->assertSame($data->idnumber, tool_uploadcourse_helper::generate_shortname($data, '%i'));
+        $this->assertSame('Ah Bh Ch', tool_uploadcourse_helper::generate_shortname($data, '%~8f'));
+        $this->assertSame('AH BH CH', tool_uploadcourse_helper::generate_shortname($data, '%+8f'));
+        $this->assertSame('id123', tool_uploadcourse_helper::generate_shortname($data, '%-i'));
+        $this->assertSame('[Ah bh Ch] = ID123', tool_uploadcourse_helper::generate_shortname($data, '[%8f] = %i'));
+        $this->assertSame('0', tool_uploadcourse_helper::generate_shortname($data, '0'));
+        $this->assertSame('%unknown', tool_uploadcourse_helper::generate_shortname($data, '%unknown'));
+
+        $this->assertNull(tool_uploadcourse_helper::generate_shortname($data, ''));
+        $this->assertNull(tool_uploadcourse_helper::generate_shortname(array(), '%f'));
+    }
+
+    public function test_get_course_formats() {
+        $result = tool_uploadcourse_helper::get_course_formats();
+        $this->assertSame(array_keys(get_plugin_list('format')), $result);
+        // Should be similar as first result, as cached.
+        $this->assertSame($result, tool_uploadcourse_helper::get_course_formats());
+    }
+
+    public function test_get_enrolment_data() {
+        $this->resetAfterTest(true);
+        $data = array(
+            'enrolment_1' => 'unknown',
+            'enrolment_1_foo' => '1',
+            'enrolment_1_bar' => '2',
+            'enrolment_2' => 'self',
+            'enrolment_2_delete' => '1',
+            'enrolment_2_foo' => 'a',
+            'enrolment_2_bar' => '1',
+            'enrolment_3' => 'manual',
+            'enrolment_3_disable' => '2',
+            'enrolment_3_foo' => 'b',
+            'enrolment_3_bar' => '2',
+            'enrolment_4' => 'database',
+            'enrolment_4_foo' => 'x',
+            'enrolment_4_bar' => '3',
+            'enrolment_5_test3' => 'test3',
+            'enrolment_5_test2' => 'test2',
+            'enrolment_5_test1' => 'test1',
+            'enrolment_5' => 'flatfile',
+        );
+        $expected = array(
+            'self' => array(
+                'delete' => '1',
+                'foo' => 'a',
+                'bar' => '1',
+            ),
+            'manual' => array(
+                'disable' => '2',
+                'foo' => 'b',
+                'bar' => '2',
+            ),
+            'database' => array(
+                'foo' => 'x',
+                'bar' => '3',
+            ),
+            'flatfile' => array(
+                'test3' => 'test3',
+                'test2' => 'test2',
+                'test1' => 'test1',
+            )
+        );
+        $this->assertSame(tool_uploadcourse_helper::get_enrolment_data($data), $expected);
+    }
+
+    public function test_get_enrolment_plugins() {
+        $this->resetAfterTest(true);
+        $actual = tool_uploadcourse_helper::get_enrolment_plugins();
+        $this->assertSame(array_keys(enrol_get_plugins(false)), array_keys($actual));
+        // This should be identical as cached.
+        $secondactual = tool_uploadcourse_helper::get_enrolment_plugins();
+        $this->assertEquals($actual, $secondactual);
+    }
+
+    public function test_get_restore_content_dir() {
+        global $CFG;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $c1 = $this->getDataGenerator()->create_course();
+        $c2 = $this->getDataGenerator()->create_course((object) array('shortname' => 'Yay'));
+
+        // Creating backup file.
+        $bc = new backup_controller(backup::TYPE_1COURSE, $c1->id, backup::FORMAT_MOODLE,
+            backup::INTERACTIVE_NO, backup::MODE_GENERAL, 2);
+        $bc->execute_plan();
+        $result = $bc->get_results();
+        $this->assertTrue(isset($result['backup_destination']));
+        $c1backupfile = $result['backup_destination']->copy_content_to_temp();
+        $bc->destroy();
+
+        // Creating backup file.
+        $bc = new backup_controller(backup::TYPE_1COURSE, $c2->id, backup::FORMAT_MOODLE,
+            backup::INTERACTIVE_NO, backup::MODE_GENERAL, 2);
+        $bc->execute_plan();
+        $result = $bc->get_results();
+        $this->assertTrue(isset($result['backup_destination']));
+        $c2backupfile = $result['backup_destination']->copy_content_to_temp();
+        $bc->destroy();
+
+        // Checking restore dir.
+        $dir = tool_uploadcourse_helper::get_restore_content_dir($c1backupfile, null);
+        $bcinfo = backup_general_helper::get_backup_information($dir);
+        $this->assertEquals($bcinfo->original_course_id, $c1->id);
+        $this->assertEquals($bcinfo->original_course_fullname, $c1->fullname);
+
+        // Do it again, it should be the same directory.
+        $dir2 = tool_uploadcourse_helper::get_restore_content_dir($c1backupfile, null);
+        $this->assertEquals($dir, $dir2);
+
+        // Get the second course.
+        $dir = tool_uploadcourse_helper::get_restore_content_dir($c2backupfile, null);
+        $bcinfo = backup_general_helper::get_backup_information($dir);
+        $this->assertEquals($bcinfo->original_course_id, $c2->id);
+        $this->assertEquals($bcinfo->original_course_fullname, $c2->fullname);
+
+        // Checking with a shortname.
+        $dir = tool_uploadcourse_helper::get_restore_content_dir(null, $c1->shortname);
+        $bcinfo = backup_general_helper::get_backup_information($dir);
+        $this->assertEquals($bcinfo->original_course_id, $c1->id);
+        $this->assertEquals($bcinfo->original_course_fullname, $c1->fullname);
+
+        // Do it again, it should be the same directory.
+        $dir2 = tool_uploadcourse_helper::get_restore_content_dir(null, $c1->shortname);
+        $this->assertEquals($dir, $dir2);
+
+        // Get the second course.
+        $dir = tool_uploadcourse_helper::get_restore_content_dir(null, $c2->shortname);
+        $bcinfo = backup_general_helper::get_backup_information($dir);
+        $this->assertEquals($bcinfo->original_course_id, $c2->id);
+        $this->assertEquals($bcinfo->original_course_fullname, $c2->fullname);
+
+        // Get a course that does not exist.
+        $errors = array();
+        $dir = tool_uploadcourse_helper::get_restore_content_dir(null, 'DoesNotExist', $errors);
+        $this->assertFalse($dir);
+        $this->assertArrayHasKey('coursetorestorefromdoesnotexist', $errors);
+
+        // Cleaning content directories.
+        $oldcfg = isset($CFG->keeptempdirectoriesonbackup) ? $CFG->keeptempdirectoriesonbackup : false;
+        $dir = "$CFG->tempdir/backup/$dir";
+        $this->assertTrue(file_exists($dir));
+
+        $CFG->keeptempdirectoriesonbackup = false;
+        tool_uploadcourse_helper::clean_restore_content();
+        $this->assertTrue(file_exists($dir));
+
+        $CFG->keeptempdirectoriesonbackup = true;
+        tool_uploadcourse_helper::clean_restore_content();
+        $this->assertFalse(file_exists($dir));
+
+        $CFG->keeptempdirectoriesonbackup = $oldcfg;
+
+        // Restore the time limit to prevent warning.
+        set_time_limit(0);
+    }
+
+    public function test_get_role_ids() {
+        $this->getDataGenerator();
+        // Mimic function result.
+        $expected = array();
+        $roles = get_all_roles();
+        foreach ($roles as $role) {
+            $expected[$role->shortname] = $role->id;
+        }
+
+        $actual = tool_uploadcourse_helper::get_role_ids();
+        $this->assertSame($actual, $expected);
+
+        // Check cache.
+        $this->assertSame($actual, tool_uploadcourse_helper::get_role_ids());
+    }
+
+    public function test_get_role_names() {
+        $this->resetAfterTest(true);
+
+        create_role('Villain', 'villain', 'The bad guys');
+        $data = array(
+            'role_student' => 'Padawan',
+            'role_teacher' => 'Guardian',
+            'role_editingteacher' => 'Knight',
+            'role_manager' => 'Master',
+            'role_villain' => 'Jabba the Hutt',
+            'role_android' => 'R2D2',
+        );
+
+        // Get the role IDs, but need to force the cache reset as a new role is defined.
+        $roleids = tool_uploadcourse_helper::get_role_ids(true);
+
+        $expected = array(
+            'role_' . $roleids['student'] => 'Padawan',
+            'role_' . $roleids['teacher'] => 'Guardian',
+            'role_' . $roleids['editingteacher'] => 'Knight',
+            'role_' . $roleids['manager'] => 'Master',
+            'role_' . $roleids['villain'] => 'Jabba the Hutt',
+        );
+
+        $errors = array();
+        $actual = tool_uploadcourse_helper::get_role_names($data, $errors);
+        $this->assertSame($actual, $expected);
+        $this->assertArrayHasKey('invalidroles', $errors);
+    }
+
+    public function test_increment_idnumber() {
+        $this->resetAfterTest(true);
+
+        $c1 = $this->getDataGenerator()->create_course(array('idnumber' => 'C1'));
+        $c2 = $this->getDataGenerator()->create_course(array('idnumber' => 'C2'));
+        $c3 = $this->getDataGenerator()->create_course(array('idnumber' => 'Yo'));
+
+        $this->assertEquals('C3', tool_uploadcourse_helper::increment_idnumber('C1'));
+        $this->assertEquals('Yo_2', tool_uploadcourse_helper::increment_idnumber('Yo'));
+        $this->assertEquals('DoesNotExist', tool_uploadcourse_helper::increment_idnumber('DoesNotExist'));
+    }
+
+    public function test_increment_shortname() {
+        $this->resetAfterTest(true);
+
+        $c1 = $this->getDataGenerator()->create_course(array('shortname' => 'C1'));
+        $c2 = $this->getDataGenerator()->create_course(array('shortname' => 'C2'));
+        $c3 = $this->getDataGenerator()->create_course(array('shortname' => 'Yo'));
+
+        // FYI: increment_shortname assumes that the course exists, and so increment the shortname immediately.
+        $this->assertEquals('C3', tool_uploadcourse_helper::increment_shortname('C1'));
+        $this->assertEquals('Yo_2', tool_uploadcourse_helper::increment_shortname('Yo'));
+        $this->assertEquals('DoesNotExist_2', tool_uploadcourse_helper::increment_shortname('DoesNotExist'));
+    }
+
+    public function test_resolve_category() {
+        $this->resetAfterTest(true);
+
+        $c1 = $this->getDataGenerator()->create_category(array('name' => 'First level'));
+        $c2 = $this->getDataGenerator()->create_category(array('name' => 'Second level', 'parent' => $c1->id));
+        $c3 = $this->getDataGenerator()->create_category(array('idnumber' => 'C3'));
+
+        $data = array(
+            'category' => $c1->id,
+            'category_path' => $c1->name . ' / ' . $c2->name,
+            'category_idnumber' => $c3->idnumber,
+        );
+
+        $this->assertEquals($c1->id, tool_uploadcourse_helper::resolve_category($data));
+        unset($data['category']);
+        $this->assertEquals($c3->id, tool_uploadcourse_helper::resolve_category($data));
+        unset($data['category_idnumber']);
+        $this->assertEquals($c2->id, tool_uploadcourse_helper::resolve_category($data));
+
+        // Adding unexisting data.
+        $errors = array();
+        $data['category_idnumber'] = 1234;
+        $this->assertEquals($c2->id, tool_uploadcourse_helper::resolve_category($data, $errors));
+        $this->assertArrayHasKey('couldnotresolvecatgorybyidnumber', $errors);
+        $errors = array();
+        $data['category'] = 1234;
+        $this->assertEquals($c2->id, tool_uploadcourse_helper::resolve_category($data, $errors));
+        $this->assertArrayHasKey('couldnotresolvecatgorybyid', $errors);
+        $errors = array();
+        $data['category_path'] = 'Not exist';
+        $this->assertEmpty(tool_uploadcourse_helper::resolve_category($data, $errors));
+        $this->assertArrayHasKey('couldnotresolvecatgorybypath', $errors);
+    }
+
+    public function test_resolve_category_by_idnumber() {
+        $this->resetAfterTest(true);
+
+        $c1 = $this->getDataGenerator()->create_category(array('idnumber' => 'C1'));
+        $c2 = $this->getDataGenerator()->create_category(array('idnumber' => 'C2'));
+
+        // Doubled for cache check.
+        $this->assertEquals($c1->id, tool_uploadcourse_helper::resolve_category_by_idnumber('C1'));
+        $this->assertEquals($c1->id, tool_uploadcourse_helper::resolve_category_by_idnumber('C1'));
+        $this->assertEquals($c2->id, tool_uploadcourse_helper::resolve_category_by_idnumber('C2'));
+        $this->assertEquals($c2->id, tool_uploadcourse_helper::resolve_category_by_idnumber('C2'));
+        $this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_idnumber('DoesNotExist'));
+        $this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_idnumber('DoesNotExist'));
+    }
+
+    public function test_resolve_category_by_path() {
+        $this->resetAfterTest(true);
+
+        $cat1 = $this->getDataGenerator()->create_category(array('name' => 'Cat 1'));
+        $cat1_1 = $this->getDataGenerator()->create_category(array('name' => 'Cat 1.1', 'parent' => $cat1->id));
+        $cat1_1_1 = $this->getDataGenerator()->create_category(array('name' => 'Cat 1.1.1', 'parent' => $cat1_1->id));
+        $cat1_1_2 = $this->getDataGenerator()->create_category(array('name' => 'Cat 1.1.2', 'parent' => $cat1_1->id));
+        $cat1_2 = $this->getDataGenerator()->create_category(array('name' => 'Cat 1.2', 'parent' => $cat1->id));
+
+        $cat2 = $this->getDataGenerator()->create_category(array('name' => 'Cat 2'));
+        $cat2_1 = $this->getDataGenerator()->create_category(array('name' => 'Cat 2.1', 'parent' => $cat2->id, 'visible' => false));
+        $cat2_1_1 = $this->getDataGenerator()->create_category(array('name' => 'Cat 2.1.1', 'parent' => $cat2_1->id));
+        $cat2_1_2 = $this->getDataGenerator()->create_category(array('name' => 'Cat 2.1.2', 'parent' => $cat2_1->id));
+        $cat2_2 = $this->getDataGenerator()->create_category(array('name' => 'Cat 2.2', 'parent' => $cat2->id));
+
+        $cat3 = $this->getDataGenerator()->create_category(array('name' => 'Cat 3'));
+        $cat3_1 = $this->getDataGenerator()->create_category(array('name' => 'Cat 3.1 Doubled', 'parent' => $cat3->id));
+        $cat3_1b = $this->getDataGenerator()->create_category(array('name' => 'Cat 3.1 Doubled', 'parent' => $cat3->id));
+        $cat3_1_1 = $this->getDataGenerator()->create_category(array('name' => 'Cat 3.1.1', 'parent' => $cat3_1->id));
+        $cat3_fakedouble = $this->getDataGenerator()->create_category(array('name' => 'Cat 3.1.1', 'parent' => $cat3->id));
+
+        // Existing categories. Doubled for cache testing.
+        $path = array('Cat 1');
+        $this->assertEquals($cat1->id, tool_uploadcourse_helper::resolve_category_by_path($path));
+        $this->assertEquals($cat1->id, tool_uploadcourse_helper::resolve_category_by_path($path));
+
+        $path = array('Cat 1', 'Cat 1.1', 'Cat 1.1.2');
+        $this->assertEquals($cat1_1_2->id, tool_uploadcourse_helper::resolve_category_by_path($path));
+        $this->assertEquals($cat1_1_2->id, tool_uploadcourse_helper::resolve_category_by_path($path));
+
+        $path = array('Cat 1', 'Cat 1.2');
+        $this->assertEquals($cat1_2->id, tool_uploadcourse_helper::resolve_category_by_path($path));
+        $this->assertEquals($cat1_2->id, tool_uploadcourse_helper::resolve_category_by_path($path));
+
+        $path = array('Cat 2');
+        $this->assertEquals($cat2->id, tool_uploadcourse_helper::resolve_category_by_path($path));
+        $this->assertEquals($cat2->id, tool_uploadcourse_helper::resolve_category_by_path($path));
+
+        // Hidden category.
+        $path = array('Cat 2', 'Cat 2.1');
+        $this->assertEquals($cat2_1->id, tool_uploadcourse_helper::resolve_category_by_path($path));
+        $this->assertEquals($cat2_1->id, tool_uploadcourse_helper::resolve_category_by_path($path));
+
+        // Hidden parent.
+        $path = array('Cat 2', 'Cat 2.1', 'Cat 2.1.2');
+        $this->assertEquals($cat2_1_2->id, tool_uploadcourse_helper::resolve_category_by_path($path));
+        $this->assertEquals($cat2_1_2->id, tool_uploadcourse_helper::resolve_category_by_path($path));
+
+        // Does not exist.
+        $path = array('No cat 3', 'Cat 1.2');
+        $this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_path($path));
+        $this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_path($path));
+
+        $path = array('Cat 2', 'Cat 2.x');
+        $this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_path($path));
+        $this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_path($path));
+
+        // Name conflict.
+        $path = array('Cat 3', 'Cat 3.1 Doubled');
+        $this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_path($path));
+        $this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_path($path));
+
+        $path = array('Cat 3', 'Cat 3.1 Doubled', 'Cat 3.1.1');
+        $this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_path($path));
+        $this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_path($path));
+
+        $path = array('Cat 3', 'Cat 3.1.1');
+        $this->assertEquals($cat3_fakedouble->id, tool_uploadcourse_helper::resolve_category_by_path($path));
+        $this->assertEquals($cat3_fakedouble->id, tool_uploadcourse_helper::resolve_category_by_path($path));
+    }
+}
diff --git a/admin/tool/uploadcourse/tests/processor_test.php b/admin/tool/uploadcourse/tests/processor_test.php
new file mode 100644 (file)
index 0000000..648fcc6
--- /dev/null
@@ -0,0 +1,225 @@
+<?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/>.
+
+/**
+ * File containing tests for the processor.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/csvlib.class.php');
+
+/**
+ * Processor test case.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_uploadcourse_processor_testcase extends advanced_testcase {
+
+    public function test_basic() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $content = array(
+            "shortname,fullname,summary",
+            "c1,Course 1,Course 1 summary",
+            "c2,Course 2,Course 2 summary",
+        );
+        $content = implode("\n", $content);
+        $iid = csv_import_reader::get_new_iid('uploadcourse');
+        $cir = new csv_import_reader($iid, 'uploadcourse');
+        $cir->load_csv_content($content, 'utf-8', 'comma');
+        $cir->init();
+
+        $options = array('mode' => tool_uploadcourse_processor::MODE_CREATE_ALL);
+        $defaults = array('category' => '1');
+
+        $p = new tool_uploadcourse_processor($cir, $options, $defaults);
+        $this->assertFalse($DB->record_exists('course', array('shortname' => 'c1')));
+        $this->assertFalse($DB->record_exists('course', array('shortname' => 'c2')));
+        $p->execute();
+        $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1')));
+        $this->assertTrue($DB->record_exists('course', array('shortname' => 'c2')));
+    }
+
+    public function test_restore_template_course() {
+        global $DB;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $c1 = $this->getDataGenerator()->create_course();
+        $c1f1 = $this->getDataGenerator()->create_module('forum', array('course' => $c1->id));
+
+        $content = array(
+            "shortname,fullname,summary",
+            "c2,Course 2,Course 2 summary",
+        );
+        $content = implode("\n", $content);
+        $iid = csv_import_reader::get_new_iid('uploadcourse');
+        $cir = new csv_import_reader($iid, 'uploadcourse');
+        $cir->load_csv_content($content, 'utf-8', 'comma');
+        $cir->init();
+
+        $options = array('mode' => tool_uploadcourse_processor::MODE_CREATE_NEW, 'templatecourse' => $c1->shortname);
+        $defaults = array('category' => '1');
+
+        $p = new tool_uploadcourse_processor($cir, $options, $defaults);
+        $this->assertFalse($DB->record_exists('course', array('shortname' => 'c2')));
+        $p->execute();
+        $c2 = $DB->get_record('course', array('shortname' => 'c2'));
+        $modinfo = get_fast_modinfo($c2);
+        $found = false;
+        foreach ($modinfo->get_cms() as $cmid => $cm) {
+            if ($cm->modname == 'forum' && $cm->name == $c1f1->name) {
+                $found = true;
+                break;
+            }
+        }
+        $this->assertTrue($found);
+
+        // Restore the time limit to prevent warning.
+        set_time_limit(0);
+    }
+
+    public function test_restore_restore_file() {
+        global $DB;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $content = array(
+            "shortname,fullname,summary",
+            "c1,Course 1,Course 1 summary",
+        );
+        $content = implode("\n", $content);
+        $iid = csv_import_reader::get_new_iid('uploadcourse');
+        $cir = new csv_import_reader($iid, 'uploadcourse');
+        $cir->load_csv_content($content, 'utf-8', 'comma');
+        $cir->init();
+
+        $options = array(
+            'mode' => tool_uploadcourse_processor::MODE_CREATE_NEW,
+            'restorefile' => __DIR__ . '/fixtures/backup.mbz',
+            'templatecourse' => 'DoesNotExist'  // Restorefile takes priority.
+        );
+        $defaults = array('category' => '1');
+
+        $p = new tool_uploadcourse_processor($cir, $options, $defaults);
+        $this->assertFalse($DB->record_exists('course', array('shortname' => 'c1')));
+        $p->execute();
+        $c1 = $DB->get_record('course', array('shortname' => 'c1'));
+        $modinfo = get_fast_modinfo($c1);
+        $found = false;
+        foreach ($modinfo->get_cms() as $cmid => $cm) {
+            if ($cm->modname == 'glossary' && $cm->name == 'Imported Glossary') {
+                $found = true;
+                break;
+            }
+        }
+        $this->assertTrue($found);
+
+        // Restore the time limit to prevent warning.
+        set_time_limit(0);
+    }
+
+    public function test_shortname_template() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $content = array(
+            "shortname,fullname,summary,idnumber",
+            ",Course 1,C1 Summary,ID123",
+        );
+        $content = implode("\n", $content);
+        $iid = csv_import_reader::get_new_iid('uploadcourse');
+        $cir = new csv_import_reader($iid, 'uploadcourse');
+        $cir->load_csv_content($content, 'utf-8', 'comma');
+        $cir->init();
+
+        $options = array('mode' => tool_uploadcourse_processor::MODE_CREATE_NEW, 'shortnametemplate' => '%i: %f');
+        $defaults = array('category' => '1');
+
+        $p = new tool_uploadcourse_processor($cir, $options, $defaults);
+        $this->assertFalse($DB->record_exists('course', array('idnumber' => 'ID123')));
+        $p->execute();
+        $this->assertTrue($DB->record_exists('course', array('idnumber' => 'ID123')));
+        $c = $DB->get_record('course', array('idnumber' => 'ID123'));
+        $this->assertEquals('ID123: Course 1', $c->shortname);
+    }
+
+    public function test_empty_csv() {
+        $this->resetAfterTest(true);
+
+        $content = array();
+        $content = implode("\n", $content);
+        $iid = csv_import_reader::get_new_iid('uploadcourse');
+        $cir = new csv_import_reader($iid, 'uploadcourse');
+        $cir->load_csv_content($content, 'utf-8', 'comma');
+        $cir->init();
+
+        $options = array('mode' => tool_uploadcourse_processor::MODE_CREATE_NEW);
+        $this->setExpectedException('moodle_exception');
+        $p = new tool_uploadcourse_processor($cir, $options, array());
+    }
+
+    public function test_not_enough_columns() {
+        $this->resetAfterTest(true);
+
+        $content = array(
+            "shortname",
+            "c1",
+        );
+        $content = implode("\n", $content);
+        $iid = csv_import_reader::get_new_iid('uploadcourse');
+        $cir = new csv_import_reader($iid, 'uploadcourse');
+        $cir->load_csv_content($content, 'utf-8', 'comma');
+        $cir->init();
+
+        $options = array('mode' => tool_uploadcourse_processor::MODE_CREATE_NEW);
+        $this->setExpectedException('moodle_exception');
+        $p = new tool_uploadcourse_processor($cir, $options, array());
+    }
+
+    public function test_preview() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $content = array(
+            "shortname,fullname,summary",
+            "c1,Course 1,Course 1 summary",
+            "c2,Course 2,Course 2 summary",
+        );
+        $content = implode("\n", $content);
+        $iid = csv_import_reader::get_new_iid('uploadcourse');
+        $cir = new csv_import_reader($iid, 'uploadcourse');
+        $cir->load_csv_content($content, 'utf-8', 'comma');
+        $cir->init();
+
+        $options = array('mode' => tool_uploadcourse_processor::MODE_CREATE_ALL);
+        $defaults = array('category' => '1');
+
+        $p = new tool_uploadcourse_processor($cir, $options, $defaults);
+        // Nothing special to expect here, just make sure no exceptions are thrown.
+        $p->preview();
+    }
+
+}
similarity index 65%
rename from enrol/authorize/version.php
rename to admin/tool/uploadcourse/version.php
index 1a91383..112b41c 100644 (file)
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Authorize.Net enrolment plugin version specification.
+ * Plugin version info.
  *
- * @package    enrol_authorize
- * @copyright  2010 Eugene Venter
- * @author     Eugene Venter
+ * @package    tool_uploadcourse
+ * @copyright  2011 Piers Harding
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2013050100;        // The current plugin version (Date: YYYYMMDDXX)
-$plugin->requires  = 2013050100;        // Requires this Moodle version
-$plugin->component = 'enrol_authorize'; // Full name of the plugin (used for diagnostics)
-$plugin->cron      = 180;
+$plugin->version   = 2013070200;            // The current plugin version (Date: YYYYMMDDXX).
+$plugin->requires  = 2013062100;            // Requires this Moodle version.
+$plugin->component = 'tool_uploadcourse';   // Full name of the plugin (used for diagnostics).
index 40a93da..1f76c1b 100644 (file)
@@ -118,7 +118,8 @@ class create_xml_file extends XMLDBAction {
     function get_plugin_type($dirpath) {
         global $CFG;
         $dirpath = $CFG->dirroot.$dirpath;
-        $plugintypes = get_plugin_types();
+        // Reverse order so that we get subplugin matches.
+        $plugintypes = array_reverse(core_component::get_plugin_types());
         foreach ($plugintypes as $plugintype => $pluginbasedir) {
             if (substr($dirpath, 0, strlen($pluginbasedir)) == $pluginbasedir) {
                 return $plugintype;
index 6a1cc49..b1c09ae 100644 (file)
@@ -88,7 +88,7 @@ class external_service_form extends moodleform {
 
         // Prepare the list of capabilities to choose from
         $systemcontext = context_system::instance();
-        $allcapabilities = fetch_context_capabilities($systemcontext);
+        $allcapabilities = $systemcontext->get_capabilities();
         $capabilitychoices = array();
         $capabilitychoices['norequiredcapability'] = get_string('norequiredcapability',
                         'webservice');
index aff143f..d25e464 100644 (file)
@@ -2029,7 +2029,6 @@ class restore_badges_structure_step extends restore_structure_step {
         $params = array(
                 'name'           => $data->name,
                 'description'    => $data->description,
-                'image'          => 0,
                 'timecreated'    => $this->apply_date_offset($data->timecreated),
                 'timemodified'   => $this->apply_date_offset($data->timemodified),
                 'usercreated'    => $data->usercreated,
index 7feafe8..e7a2f91 100644 (file)
@@ -949,21 +949,25 @@ abstract class restore_dbops {
                 } else {
                     // This backup does not include the files - they should be available in moodle filestorage already.
 
-                    // Even if a file has been deleted since the backup was made, the file metadata will remain in the
-                    // files table, and the file will not be moved to the trashdir.
-                    // Files are not cleared from the files table by cron until several days after deletion.
-                    if ($foundfiles = $DB->get_records('files', array('contenthash' => $file->contenthash))) {
-                        // Only grab one of the foundfiles - the file content should be the same for all entries.
-                        $foundfile = reset($foundfiles);
-                        $fs->create_file_from_storedfile($file_record, $foundfile->id);
-                    } else {
-                        // A matching existing file record was not found in the database.
-                        $result = new stdClass();
-                        $result->code = 'file_missing_in_backup';
-                        $result->message = sprintf('missing file %s%s in backup', $file->filepath, $file->filename);
-                        $result->level = backup::LOG_WARNING;
-                        $results[] = $result;
-                        continue;
+                    // Create the file in the filepool if it does not exist yet.
+                    if (!$fs->file_exists($newcontextid, $component, $filearea, $rec->newitemid, $file->filepath, $file->filename)) {
+
+                        // Even if a file has been deleted since the backup was made, the file metadata will remain in the
+                        // files table, and the file will not be moved to the trashdir.
+                        // Files are not cleared from the files table by cron until several days after deletion.
+                        if ($foundfiles = $DB->get_records('files', array('contenthash' => $file->contenthash))) {
+                            // Only grab one of the foundfiles - the file content should be the same for all entries.
+                            $foundfile = reset($foundfiles);
+                            $fs->create_file_from_storedfile($file_record, $foundfile->id);
+                        } else {
+                            // A matching existing file record was not found in the database.
+                            $result = new stdClass();
+                            $result->code = 'file_missing_in_backup';
+                            $result->message = sprintf('missing file %s%s in backup', $file->filepath, $file->filename);
+                            $result->level = backup::LOG_WARNING;
+                            $results[] = $result;
+                            continue;
+                        }
                     }
                 }
 
index 413e56c..91c316c 100644 (file)
@@ -80,7 +80,6 @@ if ($form->is_cancelled()) {
     $fordb->timemodified = $now;
     $fordb->usercreated = $USER->id;
     $fordb->usermodified = $USER->id;
-    $fordb->image = 0;
     $fordb->issuername = $data->issuername;
     $fordb->issuerurl = $data->issuerurl;
     $fordb->issuercontact = $data->issuercontact;
index 37c6707..b51d155 100644 (file)
@@ -46,7 +46,6 @@ class badges_testcase extends advanced_testcase {
         $fordb->timemodified = time();
         $fordb->usercreated = $user->id;
         $fordb->usermodified = $user->id;
-        $fordb->image = 0;
         $fordb->issuername = "Test issuer";
         $fordb->issuerurl = "http://issuer-url.domain.co.nz";
         $fordb->expiredate = null;
@@ -74,7 +73,6 @@ class badges_testcase extends advanced_testcase {
         $newid = $badge->make_clone();
         $cloned_badge = new badge($newid);
 
-        $this->assertEquals($badge->image, $cloned_badge->image);
         $this->assertEquals($badge->description, $cloned_badge->description);
         $this->assertEquals($badge->issuercontact, $cloned_badge->issuercontact);
         $this->assertEquals($badge->issuername, $cloned_badge->issuername);
diff --git a/blocks/dock.js b/blocks/dock.js
deleted file mode 100644 (file)
index 95d0a3e..0000000
+++ /dev/null
@@ -1,1141 +0,0 @@
-/**
- * The dock namespace: Contains all things dock related
- * @namespace
- */
-M.core_dock = {
-    count : 0,              // The number of dock items currently
-    totalcount : 0,         // The number of dock items through the page life
-    items : [],             // An array of dock items
-    earlybinds : [],        // Events added before the dock was augmented to support events
-    Y : null,               // The YUI instance to use with dock related code
-    initialised : false,    // True once thedock has been initialised
-    delayedevent : null,    // Will be an object if there is a delayed event in effect
-    preventevent : null,    // Will be an eventtype if there is an eventyoe to prevent
-    holdingarea : null
-};
-/**
- * Namespace containing the nodes that relate to the dock
- * @namespace
- */
-M.core_dock.nodes = {
-    dock : null, // The dock itself
-    body : null, // The body of the page
-    panel : null // The docks panel
-};
-/**
- * Configuration parameters used during the initialisation and setup
- * of dock and dock items.
- * This is here specifically so that themers can override core parameters and
- * design aspects without having to re-write navigation
- * @namespace
- */
-M.core_dock.cfg = {
-    buffer:10,                          // Buffer used when containing a panel
-    position:'left',                    // position of the dock
-    orientation:'vertical',             // vertical || horizontal determines if we change the title
-    spacebeforefirstitem: 10,           // Space between the top of the dock and the first item
-    removeallicon: M.util.image_url('t/dock_to_block', 'moodle')
-};
-/**
- * CSS classes to use with the dock
- * @namespace
- */
-M.core_dock.css = {
-    dock:'dock',                    // CSS Class applied to the dock box
-    dockspacer:'dockspacer',        // CSS class applied to the dockspacer
-    controls:'controls',            // CSS class applied to the controls box
-    body:'has_dock',                // CSS class added to the body when there is a dock
-    buttonscontainer: 'buttons_container',
-    dockeditem:'dockeditem',        // CSS class added to each item in the dock
-    dockeditemcontainer:'dockeditem_container',
-    dockedtitle:'dockedtitle',      // CSS class added to the item's title in each dock
-    activeitem:'activeitem'         // CSS class added to the active item
-};
-/**
- * Augments the classes as required and processes early bindings
- */
-M.core_dock.init = function(Y) {
-    if (this.initialised) {
-        return true;
-    }
-    var css = this.css;
-    this.initialised = true;
-    this.Y = Y;
-    this.nodes.body = Y.one(document.body);
-
-    // Give the dock item class the event properties/methods
-    Y.augment(this.item, Y.EventTarget);
-    Y.augment(this, Y.EventTarget, true);
-    /**
-     * A 'dock:actionkey' Event.
-     * The event consists of the left arrow, right arrow, enter and space keys.
-     * More keys can be mapped to action meanings.
-     * actions: collapse , expand, toggle, enter.
-     *
-     * This event is subscribed to by dockitems.
-     * The on() method to subscribe allows specifying the desired trigger actions as JSON.
-     *
-     * This event can also be delegated if needed.
-     * Todo: This could be centralised, a similar Event is defined in blocks/navigation/yui/navigation/navigation.js
-     */
-    Y.Event.define("dock:actionkey", {
-        // Webkit and IE repeat keydown when you hold down arrow keys.
-        // Opera links keypress to page scroll; others keydown.
-        // Firefox prevents page scroll via preventDefault() on either
-        // keydown or keypress.
-        _event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
-
-        _keys: {
-            //arrows
-            '37': 'collapse',
-            '39': 'expand',
-            //(@todo: lrt/rtl/M.core_dock.cfg.orientation decision to assign arrow to meanings)
-            '32': 'toggle',
-            '13': 'enter'
-        },
-
-        _keyHandler: function (e, notifier, args) {
-            if (!args.actions) {
-                var actObj = {collapse:true, expand:true, toggle:true, enter:true};
-            } else {
-                var actObj = args.actions;
-            }
-            if (this._keys[e.keyCode] && actObj[this._keys[e.keyCode]]) {
-                e.action = this._keys[e.keyCode];
-                notifier.fire(e);
-            }
-        },
-
-        on: function (node, sub, notifier) {
-            // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions).
-            if (sub.args == null) {
-                //no actions given
-                sub._detacher = node.on(this._event, this._keyHandler,this, notifier, {actions:false});
-            } else {
-                sub._detacher = node.on(this._event, this._keyHandler,this, notifier, sub.args[0]);
-            }
-        },
-
-        detach: function (node, sub, notifier) {
-            //detach our _detacher handle of the subscription made in on()
-            sub._detacher.detach();
-        },
-
-        delegate: function (node, sub, notifier, filter) {
-            // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions).
-            if (sub.args == null) {
-                //no actions given
-                sub._delegateDetacher = node.delegate(this._event, this._keyHandler,filter, this, notifier, {actions:false});
-            } else {
-                sub._delegateDetacher = node.delegate(this._event, this._keyHandler,filter, this, notifier, sub.args[0]);
-            }
-        },
-
-        detachDelegate: function (node, sub, notifier) {
-            sub._delegateDetacher.detach();
-        }
-    });
-    // Publish the events the dock has
-    this.publish('dock:beforedraw', {prefix:'dock'});
-    this.publish('dock:beforeshow', {prefix:'dock'});
-    this.publish('dock:shown', {prefix:'dock'});
-    this.publish('dock:hidden', {prefix:'dock'});
-    this.publish('dock:initialised', {prefix:'dock'});
-    this.publish('dock:itemadded', {prefix:'dock'});
-    this.publish('dock:itemremoved', {prefix:'dock'});
-    this.publish('dock:itemschanged', {prefix:'dock'});
-    this.publish('dock:panelgenerated', {prefix:'dock'});
-    this.publish('dock:panelresizestart', {prefix:'dock'});
-    this.publish('dock:resizepanelcomplete', {prefix:'dock'});
-    this.publish('dock:starting', {prefix: 'dock',broadcast:  2,emitFacade: true});
-    this.fire('dock:starting');
-    // Re-apply early bindings properly now that we can
-    this.applyBinds();
-    // Check if there is a customisation function
-    if (typeof(customise_dock_for_theme) === 'function') {
-        try {
-            // Run the customisation function
-            customise_dock_for_theme();
-        } catch (exception) {
-            // Do nothing at the moment
-        }
-    }
-
-    var dock = Y.one('#dock');
-    if (!dock) {
-        // Start the construction of the dock
-        dock = Y.Node.create('<div id="dock" role="menubar" class="'+css.dock+' '+css.dock+'_'+this.cfg.position+'_'+this.cfg.orientation+'"></div>')
-                    .append(Y.Node.create('<div class="'+css.buttonscontainer+'"></div>')
-                        .append(Y.Node.create('<div class="'+css.dockeditemcontainer+'"></div>')));
-        this.nodes.body.append(dock);
-    } else {
-        dock.addClass(css.dock+'_'+this.cfg.position+'_'+this.cfg.orientation);
-    }
-    this.holdingarea = Y.Node.create('<div></div>').setStyles({display:'none'});
-    this.nodes.body.append(this.holdingarea);
-    if (Y.UA.ie > 0 && Y.UA.ie < 7) {
-        // Adjust for IE 6 (can't handle fixed pos)
-        dock.setStyle('height', dock.get('winHeight')+'px');
-    }
-    // Store the dock
-    this.nodes.dock = dock;
-    this.nodes.buttons = dock.one('.'+css.buttonscontainer);
-    this.nodes.container = this.nodes.buttons.one('.'+css.dockeditemcontainer);
-
-    if (Y.all('.block.dock_on_load').size() == 0) {
-        // Nothing on the dock... hide it using CSS
-        dock.addClass('nothingdocked');
-    } else {
-        this.nodes.body.addClass(this.css.body).addClass(this.css.body+'_'+this.cfg.position+'_'+this.cfg.orientation);
-    }
-
-    this.fire('dock:beforedraw');
-
-    // Add a removeall button
-    // Must set the image src seperatly of we get an error with XML strict headers
-    var removeall = Y.Node.create('<img alt="'+M.str.block.undockall+'" title="'+M.str.block.undockall+'" tabindex="0"/>');
-    removeall.setAttribute('src',this.cfg.removeallicon);
-    removeall.on('removeall|click', this.remove_all, this);
-    removeall.on('dock:actionkey', this.remove_all, this, {actions:{enter:true}});
-    this.nodes.buttons.appendChild(Y.Node.create('<div class="'+css.controls+'"></div>').append(removeall));
-
-    // Create a manager for the height of the tabs. Once set this can be forgotten about
-    new (function(Y){
-        return {
-            enabled : false,        // True if the item_sizer is being used, false otherwise
-            /**
-             * Initialises the dock sizer which then attaches itself to the required
-             * events in order to monitor the dock
-             * @param {YUI} Y
-             */
-            init : function() {
-                M.core_dock.on('dock:itemschanged', this.checkSizing, this);
-                Y.on('windowresize', this.checkSizing, this);
-            },
-            /**
-             * Check if the size dock items needs to be adjusted
-             */
-            checkSizing : function() {
-                var dock = M.core_dock;
-                var possibleheight = dock.nodes.dock.get('offsetHeight') - dock.nodes.dock.one('.controls').get('offsetHeight') - (dock.cfg.buffer*3) - (dock.items.length*2);
-                var totalheight = 0;
-                for (var id in dock.items) {
-                    var dockedtitle = Y.one(dock.items[id].title).ancestor('.'+dock.css.dockedtitle);
-                    if (dockedtitle) {
-                        if (this.enabled) {
-                            dockedtitle.setStyle('height', 'auto');
-                        }
-                        totalheight += dockedtitle.get('offsetHeight') || 0;
-                    }
-                }
-                if (totalheight > possibleheight) {
-                    this.enable(possibleheight);
-                }
-            },
-            /**
-             * Enables the dock sizer and resizes where required.
-             */
-            enable : function(possibleheight) {
-                var dock = M.core_dock;
-                var runningcount = 0;
-                var usedheight = 0;
-                this.enabled = true;
-                for (var id in dock.items) {
-                    var itemtitle = Y.one(dock.items[id].title).ancestor('.'+dock.css.dockedtitle);
-                    if (!itemtitle) {
-                        continue;
-                    }
-                    var itemheight = Math.floor((possibleheight-usedheight) / (dock.count - runningcount));
-                    var offsetheight = itemtitle.get('offsetHeight');
-                    itemtitle.setStyle('overflow', 'hidden');
-                    if (offsetheight > itemheight) {
-                        itemtitle.setStyle('height', itemheight+'px');
-                        usedheight += itemheight;
-                    } else {
-                        usedheight += offsetheight;
-                    }
-                    runningcount++;
-                }
-            }
-        };
-    })(Y).init();
-
-    // Attach the required event listeners
-    // We use delegate here as that way a handful of events are created for the dock
-    // and all items rather than the same number for the dock AND every item individually
-    Y.delegate('click', this.handleEvent, this.nodes.dock, '.'+this.css.dockedtitle, this, {cssselector:'.'+this.css.dockedtitle, delay:0});
-    Y.delegate('mouseenter', this.handleEvent, this.nodes.dock, '.'+this.css.dockedtitle, this, {cssselector:'.'+this.css.dockedtitle, delay:0.5, iscontained:true, preventevent:'click', preventdelay:3});
-    //Y.delegate('mouseleave', this.handleEvent, this.nodes.body, '#dock', this,  {cssselector:'#dock', delay:0.5, iscontained:false});
-    this.nodes.dock.on('mouseleave', this.handleEvent, this, {cssselector:'#dock', delay:0.5, iscontained:false});
-
-    this.nodes.body.on('click', this.handleEvent, this,  {cssselector:'body', delay:0});
-    this.on('dock:itemschanged', this.resizeBlockSpace, this);
-    this.on('dock:itemschanged', this.checkDockVisibility, this);
-    this.on('dock:itemschanged', this.resetFirstItem, this);
-    // Inform everyone the dock has been initialised
-    this.fire('dock:initialised');
-    return true;
-};
-/**
- * Get the panel docked blocks will be shown in and initialise it if we havn't already.
- */
-M.core_dock.getPanel = function() {
-    if (this.nodes.panel === null) {
-        // Initialise the dockpanel .. should only happen once
-        this.nodes.panel = (function(Y, parent){
-            var dockpanel = Y.Node.create('<div id="dockeditempanel" class="dockitempanel_hidden"><div class="dockeditempanel_content"><div class="dockeditempanel_hd"></div><div class="dockeditempanel_bd"></div></div></div>');
-            // Give the dockpanel event target properties and methods
-            Y.augment(dockpanel, Y.EventTarget);
-            // Publish events for the dock panel
-            dockpanel.publish('dockpanel:beforeshow', {prefix:'dockpanel'});
-            dockpanel.publish('dockpanel:shown', {prefix:'dockpanel'});
-            dockpanel.publish('dockpanel:beforehide', {prefix:'dockpanel'});
-            dockpanel.publish('dockpanel:hidden', {prefix:'dockpanel'});
-            dockpanel.publish('dockpanel:visiblechange', {prefix:'dockpanel'});
-            // Cache the content nodes
-            dockpanel.contentNode = dockpanel.one('.dockeditempanel_content');
-            dockpanel.contentHeader = dockpanel.contentNode.one('.dockeditempanel_hd');
-            dockpanel.contentBody = dockpanel.contentNode.one('.dockeditempanel_bd');
-            // Set the x position of the panel
-            //dockpanel.setX(parent.get('offsetWidth'));
-            dockpanel.visible = false;
-            // Add a show event
-            dockpanel.show = function() {
-                this.fire('dockpanel:beforeshow');
-                this.visible = true;
-                this.removeClass('dockitempanel_hidden');
-                this.fire('dockpanel:shown');
-                this.fire('dockpanel:visiblechange');
-            };
-            // Add a hide event
-            dockpanel.hide = function() {
-                this.fire('dockpanel:beforehide');
-                this.visible = false;
-                this.addClass('dockitempanel_hidden');
-                this.fire('dockpanel:hidden');
-                this.fire('dockpanel:visiblechange');
-            };
-            // Add a method to set the header content
-            dockpanel.setHeader = function(content) {
-                this.contentHeader.setContent(content);
-                if (arguments.length > 1) {
-                    for (var i=1;i < arguments.length;i++) {
-                        this.contentHeader.append(arguments[i]);
-                    }
-                }
-            };
-            // Add a method to set the body content
-            dockpanel.setBody = function(content) {
-                this.contentBody.setContent(content);
-            };
-            // Add a method to set the top of the panel position
-            dockpanel.setTop = function(newtop) {
-                if (Y.UA.ie > 0 && Y.UA.ie < 7) {
-                    this.setY(newtop);
-                } else {
-                    this.setStyle('top', newtop.toString()+'px');
-                }
-                return;
-            };
-            /**
-             * Increases the width of the panel to avoid horizontal scrolling
-             * if possible.
-             */
-            dockpanel.correctWidth = function() {
-                var bd = this.one('.dockeditempanel_bd');
-
-                // Width of content
-                var w = bd.get('clientWidth');
-                // Scrollable width of content
-                var s = bd.get('scrollWidth');
-                // Width of content container with overflow
-                var ow = this.get('offsetWidth');
-                // The new width
-                var nw = w;
-                // The max width (80% of screen)
-                var mw = Math.round(this.get('winWidth') * 0.8);
-
-                // If the scrollable width is more than the visible width
-                if (s > w) {
-                    //   Content width
-                    // + the difference
-                    // + any rendering difference (borders, padding)
-                    // + 10px to make it look nice.
-                    nw = w + (s-w) + ((ow-w)*2) + 10;
-                }
-
-                // Make sure its not more then the maxwidth
-                if (nw > mw) {
-                    nw = mw;
-                }
-
-                // Set the new width if its more than the old width.
-                if (nw > ow) {
-                    this.setStyle('width', nw+'px');
-                }
-            }
-            // Put the dockpanel in the body
-            parent.append(dockpanel);
-            // Return it
-            return dockpanel;
-        })(this.Y, this.nodes.dock);
-        this.nodes.panel.on('panel:visiblechange', this.resize, this);
-        this.Y.on('windowresize', this.resize, this);
-        this.fire('dock:panelgenerated');
-    }
-    return this.nodes.panel;
-};
-/**
- * Handles a generic event within the dock
- * @param {Y.Event} e
- * @param {object} options Event configuration object
- */
-M.core_dock.handleEvent = function(e, options) {
-    var item = this.getActiveItem();
-    if (options.cssselector == 'body') {
-        if (!this.nodes.dock.contains(e.target)) {
-            if (item) {
-                item.hide();
-            }
-        }
-    } else {
-        var target;
-        if (e.target.test(options.cssselector)) {
-            target = e.target;
-        } else {
-            target = e.target.ancestor(options.cssselector);
-        }
-        if (!target) {
-            return true;
-        }
-        if (this.preventevent !== null && e.type === this.preventevent) {
-            return true;
-        }
-        if (options.preventevent) {
-            this.preventevent = options.preventevent;
-            if (options.preventdelay) {
-                setTimeout(function(){M.core_dock.preventevent = null;}, options.preventdelay*1000);
-            }
-        }
-        if (this.delayedevent && this.delayedevent.timeout) {
-            clearTimeout(this.delayedevent.timeout);
-            this.delayedevent.event.detach();
-            this.delayedevent = null;
-        }
-        if (options.delay > 0) {
-            return this.delayEvent(e, options, target);
-        }
-        var targetid = target.get('id');
-        if (targetid.match(/^dock_item_(\d+)_title$/)) {
-            item = this.items[targetid.replace(/^dock_item_(\d+)_title$/, '$1')];
-            if (item.active) {
-                item.hide();
-            } else {
-                item.show();
-            }
-        } else if (item) {
-            item.hide();
-        }
-    }
-    return true;
-};
-/**
- * This function delays an event and then fires it providing the cursor if either
- * within or outside of the original target (options.iscontained=true|false)
- * @param {Y.Event} event
- * @param {object} options
- * @param {Y.Node} target
- * @return bool
- */
-M.core_dock.delayEvent = function(event, options, target) {
-    var self = this;
-    self.delayedevent = (function(){
-        return {
-            target : target,
-            event : self.nodes.body.on('mousemove', function(e){
-                self.delayedevent.target = e.target;
-            }),
-            timeout : null
-        };
-    })(self);
-    self.delayedevent.timeout = setTimeout(function(){
-        self.delayedevent.timeout = null;
-        self.delayedevent.event.detach();
-        if (options.iscontained == self.nodes.dock.contains(self.delayedevent.target)) {
-            self.handleEvent(event, {cssselector:options.cssselector, delay:0, iscontained:options.iscontained});
-        }
-    }, options.delay*1000);
-    return true;
-};
-/**
- * Corrects the orientation of the title, which for the default
- * dock just means making it vertical
- * The orientation is determined by M.str.langconfig.thisdirectionvertical:
- *    ver : Letters are stacked rather than rotated
- *    ttb : Title is rotated clockwise so the first letter is at the top
- *    btt : Title is rotated counterclockwise so the first letter is at the bottom.
- * @param {string} title
- */
-M.core_dock.fixTitleOrientation = function(item, title, text) {
-    var Y = this.Y;
-
-    var title = Y.one(title);
-
-    if(M.core_dock.cfg.orientation != 'vertical') {
-        // If the dock isn't vertical don't adjust it!
-        title.setContent(text);
-        return title
-    }
-
-    if (Y.UA.ie > 0 && Y.UA.ie < 8) {
-        // IE 6/7 can't rotate text so force ver
-        M.str.langconfig.thisdirectionvertical = 'ver';
-    }
-
-    var clockwise = false;
-    switch (M.str.langconfig.thisdirectionvertical) {
-        case 'ver':
-            // Stacked is easy
-            return title.setContent(text.split('').join('<br />'));
-        case 'ttb':
-            clockwise = true;
-            break;
-        case 'btt':
-            clockwise = false;
-            break;
-    }
-
-    if (Y.UA.ie == 8) {
-        // IE8 can flip the text via CSS but not handle transform. IE9+ can handle the CSS3 transform attribute.
-        title.setContent(text);
-        title.setAttribute('style', 'writing-mode: tb-rl; filter: flipV flipH;display:inline;');
-        title.addClass('filterrotate');
-        return title;
-    }
-
-    // We need to fix a font-size - sorry theme designers.
-    var fontsize = '11px';
-    var transform = (clockwise) ? 'rotate(90deg)' : 'rotate(270deg)';
-    var test = Y.Node.create('<h2><span class="transform-test-node" style="font-size:'+fontsize+';">'+text+'</span></h2>');
-    this.nodes.body.insert(test, 0);
-    var width = test.one('span').get('offsetWidth') * 1.2;
-    var height = test.one('span').get('offsetHeight');
-    test.remove();
-
-    title.setContent(text);
-    title.addClass('css3transform');
-
-    // Move the title into position
-    title.setStyles({
-        'margin' : '0',
-        'padding' : '0',
-        'position' : 'relative',
-        'fontSize' : fontsize,
-        'width' : width,
-        'top' : width/2
-    });
-
-    // Positioning is different when in RTL mode.
-    if (right_to_left()) {
-        title.setStyle('left', width/2 - height);
-    } else {
-        title.setStyle('right', width/2 - height);
-    }
-
-    // Rotate the text
-    title.setStyles({
-        'transform' : transform,
-        '-ms-transform' : transform,
-        '-moz-transform' : transform,
-        '-webkit-transform' : transform,
-        '-o-transform' : transform
-    });
-
-    var container = Y.Node.create('<div></div>');
-    container.append(title);
-    container.setStyle('height', width + (width / 4));
-    container.setStyle('position', 'relative');
-    return container;
-
-    return title;
-};
-/**
- * Resizes the space that contained blocks if there were no blocks left in
- * it. e.g. if all blocks have been moved to the dock
- * @param {Y.Node} node
- */
-M.core_dock.resizeBlockSpace = function(node) {
-
-    if (this.Y.all('.block.dock_on_load').size()>0) {
-        // Do not resize during initial load
-        return;
-    }
-    var blockregions = [];
-    var populatedblockregions = 0;
-    this.Y.all('.block-region').each(function(region){
-        var hasblocks = (region.all('.block').size() > 0);
-        if (hasblocks) {
-            populatedblockregions++;
-        }
-        blockregions[region.get('id')] = {hasblocks: hasblocks, bodyclass: region.get('id').replace(/^region\-/, 'side-')+'-only'};
-    });
-    var bodynode = M.core_dock.nodes.body;
-    var showregions = false;
-    if (bodynode.hasClass('blocks-moving')) {
-        // open up blocks during blocks positioning
-        showregions = true;
-    }
-
-    var noblocksbodyclass = 'content-only';
-    var i = null;
-    if (populatedblockregions==0 && showregions==false) {
-        bodynode.addClass(noblocksbodyclass);
-        for (i in blockregions) {
-            bodynode.removeClass(blockregions[i].bodyclass);
-        }
-    } else if (populatedblockregions==1 && showregions==false) {
-        bodynode.removeClass(noblocksbodyclass);
-        for (i in blockregions) {
-            if (!blockregions[i].hasblocks) {
-                bodynode.removeClass(blockregions[i].bodyclass);
-            } else {
-                bodynode.addClass(blockregions[i].bodyclass);
-            }
-        }
-    } else {
-        bodynode.removeClass(noblocksbodyclass);
-        for (i in blockregions) {
-            bodynode.removeClass(blockregions[i].bodyclass);
-        }
-    }
-};
-/**
- * Adds a dock item into the dock
- * @function
- * @param {M.core_dock.item} item
- */
-M.core_dock.add = function(item) {
-    item.id = this.totalcount;
-    this.count++;
-    this.totalcount++;
-    this.items[item.id] = item;
-    this.items[item.id].draw();
-    this.fire('dock:itemadded', item);
-    this.fire('dock:itemschanged', item);
-};
-/**
- * Appends a dock item to the dock
- * @param {YUI.Node} docknode
- */
-M.core_dock.append = function(docknode) {
-    this.nodes.container.append(docknode);
-};
-/**
- * Initialises a generic block object
- * @param {YUI} Y
- * @param {int} id
- */
-M.core_dock.init_genericblock = function(Y, id) {
-    if (!this.initialised) {
-        this.init(Y);
-    }
-    new this.genericblock(id).initialise_block(Y, Y.one('#inst'+id));
-};
-/**
- * Removes the node at the given index and puts it back into conventional page sturcture
- * @function
- * @param {int} uid Unique identifier for the block
- * @return {boolean}
- */
-M.core_dock.remove = function(uid) {
-    if (!this.items[uid]) {
-        return false;
-    }
-    this.items[uid].remove();
-    delete this.items[uid];
-    this.count--;
-    this.fire('dock:itemremoved', uid);
-    this.fire('dock:itemschanged', uid);
-    return true;
-};
-/**
- * Ensures the the first item in the dock has the correct class
- */
-M.core_dock.resetFirstItem = function() {
-    this.nodes.dock.all('.'+this.css.dockeditem+'.firstdockitem').removeClass('firstdockeditem');
-    if (this.nodes.dock.one('.'+this.css.dockeditem)) {
-        this.nodes.dock.one('.'+this.css.dockeditem).addClass('firstdockitem');
-    }
-};
-/**
- * Removes all nodes and puts them back into conventional page sturcture
- * @function
- * @return {boolean}
- */
-M.core_dock.remove_all = function(e) {
-    for (var i in this.items) {
-        this.remove(i);
-    }
-    return true;
-};
-/**
- * Hides the active item
- */
-M.core_dock.hideActive = function() {
-    var item = this.getActiveItem();
-    if (item) {
-        item.hide();
-    }
-};
-/**
- * Checks wether the dock should be shown or hidden
- */
-M.core_dock.checkDockVisibility = function() {
-    if (!this.count) {
-        this.nodes.dock.addClass('nothingdocked');
-        this.nodes.body.removeClass(this.css.body)
-                       .removeClass(this.css.body+'_'+this.cfg.position+'_'+this.cfg.orientation);
-        this.fire('dock:hidden');
-    } else {
-        this.fire('dock:beforeshow');
-        this.nodes.dock.removeClass('nothingdocked');
-        this.nodes.body.addClass(this.css.body)
-                       .addClass(this.css.body+'_'+this.cfg.position+'_'+this.cfg.orientation);
-        this.fire('dock:shown');
-    }
-};
-/**
- * This smart little function allows developers to attach event listeners before
- * the dock has been augmented to allows event listeners.
- * Once the augmentation is complete this function will be replaced with the proper
- * on method for handling event listeners.
- * Finally applyBinds needs to be called in order to properly bind events.
- * @param {string} event
- * @param {function} callback
- */
-M.core_dock.on = function(event, callback) {
-    this.earlybinds.push({event:event,callback:callback});
-};
-/**
- * This function takes all early binds and attaches them as listeners properly
- * This should only be called once augmentation is complete.
- */
-M.core_dock.applyBinds = function() {
-    for (var i in this.earlybinds) {
-        var bind = this.earlybinds[i];
-        this.on(bind.event, bind.callback);
-    }
-    this.earlybinds = [];
-};
-/**
- * This function checks the size and position of the panel and moves/resizes if
- * required to keep it within the bounds of the window.
- */
-M.core_dock.resize = function() {
-    this.fire('dock:panelresizestart');
-    var panel = this.getPanel();
-    var item = this.getActiveItem();
-    if (!panel.visible || !item) {
-        return;
-    }
-
-    if (this.cfg.orientation=='vertical') {
-        var buffer = this.cfg.buffer;
-        var screenheight = parseInt(this.nodes.body.get('winHeight'))-(buffer*2);
-        var docky = this.nodes.dock.getY();
-        var titletop = item.nodes.docktitle.getY()-docky-buffer;
-        var containery = this.nodes.container.getY();
-        var containerheight = containery-docky+this.nodes.buttons.get('offsetHeight');
-        var scrolltop = panel.contentBody.get('scrollTop');
-        panel.contentBody.setStyle('height', 'auto');
-        panel.removeClass('oversized_content');
-        var panelheight = panel.get('offsetHeight');
-
-        if (this.Y.UA.ie > 0 && this.Y.UA.ie < 7) {
-            panel.setTop(item.nodes.docktitle.getY());
-        } else if (panelheight > screenheight) {
-            panel.setTop(buffer-containerheight);
-            panel.contentBody.setStyle('height', (screenheight-panel.contentHeader.get('offsetHeight'))+'px');
-            panel.addClass('oversized_content');
-        } else if (panelheight > (screenheight-(titletop-buffer))) {
-            var difference = panelheight - (screenheight-titletop);
-            panel.setTop(titletop-containerheight-difference+buffer);
-        } else {
-            panel.setTop(titletop-containerheight+buffer);
-        }
-
-        if (scrolltop) {
-            panel.contentBody.set('scrollTop', scrolltop);
-        }
-    }
-
-    if (this.cfg.position=='right') {
-        panel.setStyle('left', -panel.get('offsetWidth')+'px');
-
-    } else if (this.cfg.position=='top') {
-        var dockx = this.nodes.dock.getX();
-        var titleleft = item.nodes.docktitle.getX()-dockx;
-        panel.setStyle('left', titleleft+'px');
-    }
-
-    this.fire('dock:resizepanelcomplete');
-    return;
-};
-/**
- * Returns the currently active dock item or false
- */
-M.core_dock.getActiveItem = function() {
-    for (var i in this.items) {
-        if (this.items[i].active) {
-            return this.items[i];
-        }
-    }
-    return false;
-};
-/**
- * This class represents a generic block
- * @class M.core_dock.genericblock
- * @constructor
- */
-M.core_dock.genericblock = function(id) {
-    // Nothing to actually do here but it needs a constructor!
-    if (id) {
-        this.id = id;
-    }
-};
-M.core_dock.genericblock.prototype = {
-    Y : null,                   // A YUI instance to use with the block
-    id : null,                  // The block instance id
-    cachedcontentnode : null,   // The cached content node for the actual block
-    blockspacewidth : null,     // The width of the block's original container
-    skipsetposition : false,    // If true the user preference isn't updated
-    isdocked : false,           // True if it is docked
-    /**
-     * This function should be called within the block's constructor and is used to
-     * set up the initial controls for swtiching block position as well as an initial
-     * moves that may be required.
-     *
-     * @param {YUI} Y
-     * @param {YUI.Node} node The node that contains all of the block's content
-     * @return {M.core_dock.genericblock}
-     */
-    initialise_block : function(Y, node) {
-        M.core_dock.init(Y);
-
-        this.Y = Y;
-        if (!node) {
-            return false;
-        }
-
-        var commands = node.one('.header .title .commands');
-        if (!commands) {
-            commands = this.Y.Node.create('<div class="commands"></div>');
-            if (node.one('.header .title')) {
-                node.one('.header .title').append(commands);
-            }
-        }
-
-        // Must set the image src seperatly of we get an error with XML strict headers
-        var moveto = Y.Node.create('<input type="image" class="moveto customcommand requiresjs" />');
-        var header = node.one('.header .title h2');
-        moveto.setAttribute('alt', Y.Escape.html(M.util.get_string('addtodock', 'block')));
-        if (header) {
-            moveto.setAttribute('title', Y.Escape.html(M.util.get_string('dockblock', 'block', header.getHTML())));
-        } else {
-            moveto.setAttribute('title', Y.Escape.html(M.util.get_string('addtodock', 'block')));
-        }
-
-        var icon = 't/block_to_dock';
-        if (right_to_left()) {
-            icon = 't/block_to_dock_rtl';
-        }
-        moveto.setAttribute('src', M.util.image_url(icon, 'moodle'));
-        moveto.on('movetodock|click', this.move_to_dock, this, commands);
-
-        var blockaction = node.one('.block_action');
-        if (blockaction) {
-            blockaction.prepend(moveto);
-        } else {
-            commands.append(moveto);
-        }
-
-        // Move the block straight to the dock if required
-        if (node.hasClass('dock_on_load')) {
-            node.removeClass('dock_on_load');
-            this.skipsetposition = true;
-            this.move_to_dock(null, commands);
-        }
-        return this;
-    },
-
-    /**
-     * This function is reponsible for moving a block from the page structure onto the
-     * dock
-     * @param {event}
-     */
-    move_to_dock : function(e, commands) {
-        if (e) {
-            e.halt(true);
-        }
-
-        var Y = this.Y;
-        var dock = M.core_dock;
-
-        var node = Y.one('#inst'+this.id);
-        var blockcontent = node.one('.content');
-        if (!blockcontent) {
-            return;
-        }
-
-        // Disable the skip anchor when docking
-        var skipanchor = node.previous();
-        if (skipanchor.hasClass('skip-block')) {
-            skipanchor.hide();
-        }
-
-        var blockclass = (function(classes){
-            var r = /(^|\s)(block_[a-zA-Z0-9_]+)(\s|$)/;
-            var m = r.exec(classes);
-            return (m)?m[2]:m;
-        })(node.getAttribute('className').toString());
-
-        this.cachedcontentnode = node;
-
-        node.replace(Y.Node.getDOMNode(Y.Node.create('<div id="content_placeholder_'+this.id+'" class="block_dock_placeholder"></div>')));
-        M.core_dock.holdingarea.append(node);
-        node = null;
-
-        var blocktitle = Y.Node.getDOMNode(this.cachedcontentnode.one('.title h2')).cloneNode(true);
-
-        var blockcommands = this.cachedcontentnode.one('.title .commands');
-        if (!blockcommands) {
-            blockcommands = Y.Node.create('<div class="commands"></div>');
-            this.cachedcontentnode.one('.title').append(blockcommands);
-        }
-
-        // Must set the image src seperatly of we get an error with XML strict headers
-        var movetoimg = Y.Node.create('<img alt="'+Y.Escape.html(M.str.block.undockitem)+'" title="'+
-            Y.Escape.html(M.util.get_string('undockblock', 'block', blocktitle.innerHTML)) +'" />');
-        var icon = 't/dock_to_block';
-        if (right_to_left()) {
-            icon = 't/dock_to_block_rtl';
-        }
-        movetoimg.setAttribute('src', M.util.image_url(icon, 'moodle'));
-        var moveto = Y.Node.create('<a class="moveto customcommand requiresjs"></a>').append(movetoimg);
-        if (location.href.match(/\?/)) {
-            moveto.set('href', location.href+'&dock='+this.id);
-        } else {
-            moveto.set('href', location.href+'?dock='+this.id);
-        }
-        blockcommands.append(moveto);
-
-        // Create a new dock item for the block
-        var dockitem = new dock.item(Y, this.id, blocktitle, blockcontent, blockcommands, blockclass);
-        // Wire the draw events to register remove events
-        dockitem.on('dockeditem:drawcomplete', function(e){
-            // check the contents block [editing=off]
-            this.contents.all('.moveto').on('returntoblock|click', function(e){
-                e.halt();
-                dock.remove(this.id);
-            }, this);
-            // check the commands block [editing=on]
-            this.commands.all('.moveto').on('returntoblock|click', function(e){
-                e.halt();
-                dock.remove(this.id);
-            }, this);
-            // Add a close icon
-            // Must set the image src seperatly of we get an error with XML strict headers
-            var closeicon = Y.Node.create('<span class="hidepanelicon" tabindex="0"><img alt="'+M.str.block.hidepanel+'" title="'+M.str.block.hidedockpanel+'" /></span>');
-            closeicon.one('img').setAttribute('src', M.util.image_url('t/dockclose', 'moodle'));
-            closeicon.on('forceclose|click', this.hide, this);
-            closeicon.on('dock:actionkey',this.hide, this, {actions:{enter:true,toggle:true}});
-            this.commands.append(closeicon);
-        }, dockitem);
-        // Register an event so that when it is removed we can put it back as a block
-        dockitem.on('dockeditem:itemremoved', this.return_to_block, this, dockitem);
-        dock.add(dockitem);
-
-        if (!this.skipsetposition) {
-            // save the users preference
-            M.util.set_user_preference('docked_block_instance_'+this.id, 1);
-        } else {
-            this.skipsetposition = false;
-        }
-
-        this.isdocked = true;
-    },
-    /**
-     * This function removes a block from the dock and puts it back into the page
-     * structure.
-     * @param {M.core_dock.class.item}
-     */
-    return_to_block : function(dockitem) {
-        var placeholder = this.Y.one('#content_placeholder_'+this.id);
-
-        // Enable the skip anchor when going back to block mode
-        var skipanchor = placeholder.previous();
-        if (skipanchor.hasClass('skip-block')) {
-            skipanchor.show();
-        }
-
-        if (this.cachedcontentnode.one('.header')) {
-            this.cachedcontentnode.one('.header').insert(dockitem.contents, 'after');
-        } else {
-            this.cachedcontentnode.insert(dockitem.contents);
-        }
-
-        placeholder.replace(this.Y.Node.getDOMNode(this.cachedcontentnode));
-        this.cachedcontentnode = this.Y.one('#'+this.cachedcontentnode.get('id'));
-
-        var commands = dockitem.commands;
-        if (commands) {
-            commands.all('.hidepanelicon').remove();
-            commands.all('.moveto').remove();
-            commands.remove();
-        }
-        this.cachedcontentnode.one('.title').append(commands);
-        this.cachedcontentnode = null;
-        M.util.set_user_preference('docked_block_instance_'+this.id, 0);
-        this.isdocked = false;
-        return true;
-    }
-};
-
-/**
- * This class represents an item in the dock
- * @class M.core_dock.item
- * @constructor
- * @param {YUI} Y The YUI instance to use for this item
- * @param {int} uid The unique ID for the item
- * @param {this.Y.Node} title
- * @param {this.Y.Node} contents
- * @param {this.Y.Node} commands
- * @param {string} blockclass
- */
-M.core_dock.item = function(Y, uid, title, contents, commands, blockclass){
-    this.Y = Y;
-    this.publish('dockeditem:drawstart', {prefix:'dockeditem'});
-    this.publish('dockeditem:drawcomplete', {prefix:'dockeditem'});
-    this.publish('dockeditem:showstart', {prefix:'dockeditem'});
-    this.publish('dockeditem:showcomplete', {prefix:'dockeditem'});
-    this.publish('dockeditem:hidestart', {prefix:'dockeditem'});
-    this.publish('dockeditem:hidecomplete', {prefix:'dockeditem'});
-    this.publish('dockeditem:itemremoved', {prefix:'dockeditem'});
-    if (uid && this.id==null) {
-        this.id = uid;
-    }
-    if (title && this.title==null) {
-        this.titlestring = title.cloneNode(true);
-        this.title = document.createElement(title.nodeName);
-        this.title = M.core_dock.fixTitleOrientation(this, this.title, this.titlestring.firstChild.nodeValue);
-    }
-    if (contents && this.contents==null) {
-        this.contents = contents;
-    }
-    if (commands && this.commands==null) {
-        this.commands = commands;
-    }
-    if (blockclass && this.blockclass==null) {
-        this.blockclass = blockclass;
-    }
-    this.nodes = (function(){
-        return {docktitle : null, dockitem : null, container: null};
-    })();
-};
-/**
- *
- */
-M.core_dock.item.prototype = {
-    Y : null,               // The YUI instance to use with this dock item
-    id : null,              // The unique id for the item
-    name : null,            // The name of the item
-    title : null,           // The title of the item
-    titlestring : null,     // The title as a plain string
-    contents : null,        // The content of the item
-    commands : null,        // The commands for the item
-    active : false,         // True if the item is being shown
-    blockclass : null,      // The class of the block this item relates to
-    nodes : null,
-    /**
-     * This function draws the item on the dock
-     */
-    draw : function() {
-        this.fire('dockeditem:drawstart');
-
-        var Y = this.Y;
-        var css = M.core_dock.css;
-
-        this.nodes.docktitle = Y.Node.create('<div id="dock_item_'+this.id+'_title" role="menu" aria-haspopup="true" class="'+css.dockedtitle+'"></div>');
-        this.nodes.docktitle.append(this.title);
-        this.nodes.dockitem = Y.Node.create('<div id="dock_item_'+this.id+'" class="'+css.dockeditem+'" tabindex="0"></div>');
-        this.nodes.dockitem.on('dock:actionkey', this.toggle, this);
-        if (M.core_dock.count === 1) {
-            this.nodes.dockitem.addClass('firstdockitem');
-        }
-        this.nodes.dockitem.append(this.nodes.docktitle);
-        M.core_dock.append(this.nodes.dockitem);
-        this.fire('dockeditem:drawcomplete');
-        return true;
-    },
-    /**
-     * This function toggles makes the item active and shows it
-     */
-    show : function() {
-        M.core_dock.hideActive();
-        var Y = this.Y;
-        var css = M.core_dock.css;
-        var panel = M.core_dock.getPanel();
-        this.fire('dockeditem:showstart');
-        panel.setHeader(this.titlestring, this.commands);
-        panel.setBody(Y.Node.create('<div class="'+this.blockclass+' block_docked"></div>').append(this.contents));
-        panel.show();
-        panel.correctWidth();
-
-        this.active = true;
-        // Add active item class first up
-        this.nodes.docktitle.addClass(css.activeitem);
-        // Set aria-exapanded property to true.
-        this.nodes.docktitle.set('aria-expanded', "true");
-        this.fire('dockeditem:showcomplete');
-        M.core_dock.resize();
-        return true;
-    },
-    /**
-     * This function hides the item and makes it inactive
-     */
-    hide : function() {
-        var css = M.core_dock.css;
-        this.fire('dockeditem:hidestart');
-        // No longer active
-        this.active = false;
-        // Remove the active class
-        this.nodes.docktitle.removeClass(css.activeitem);
-        // Hide the panel
-        M.core_dock.getPanel().hide();
-        // Set aria-exapanded property to false
-        this.nodes.docktitle.set('aria-expanded', "false");
-        this.fire('dockeditem:hidecomplete');
-    },
-    /**
-     * A toggle between calling show and hide functions based on css.activeitem
-     * Applies rules to key press events (dock:actionkey)
-     * @param {Event} e
-     */
-    toggle : function(e) {
-        var css = M.core_dock.css;
-        if (this.nodes.docktitle.hasClass(css.activeitem) && !(e.type == 'dock:actionkey' && e.action=='expand')) {
-            this.hide();
-        } else if (!this.nodes.docktitle.hasClass(css.activeitem) && !(e.type == 'dock:actionkey' && e.action=='collapse'))  {
-            this.show();
-        }
-    },
-    /**
-     * This function removes the node and destroys it's bits
-     * @param {Event} e
-     */
-    remove : function () {
-        this.hide();
-        this.nodes.dockitem.remove();
-        this.fire('dockeditem:itemremoved');
-    }
-};
index 1aaaa1d..9d9b289 100644 (file)
@@ -230,7 +230,7 @@ class block_base {
         global $CFG;
 
         $bc = new block_contents($this->html_attributes());
-
+        $bc->attributes['data-block'] = $this->name();
         $bc->blockinstanceid = $this->instance->id;
         $bc->blockpositionid = $this->instance->blockpositionid;
 
@@ -271,6 +271,10 @@ class block_base {
             $bc->collapsible = block_contents::VISIBLE;
         }
 
+        if ($this->instance_can_be_docked() && !$this->hide_header()) {
+            $bc->dockable = true;
+        }
+
         $bc->annotation = ''; // TODO MDL-19398 need to work out what to say here.
 
         return $bc;
@@ -435,9 +439,13 @@ class block_base {
         $this->specialization();
     }
 
+    /**
+     * Allows the block to load any JS it requires into the page.
+     *
+     * By default this function simply permits the user to dock the block if it is dockable.
+     */
     function get_required_javascript() {
         if ($this->instance_can_be_docked() && !$this->hide_header()) {
-            $this->page->requires->js_init_call('M.core_dock.init_genericblock', array($this->instance->id));
             user_preference_allow_ajax_update('docked_block_instance_'.$this->instance->id, PARAM_INT);
         }
     }
index d987e4c..2252e85 100644 (file)
@@ -56,7 +56,6 @@ class block_navigation extends block_base {
      * Set the initial properties for the block
      */
     function init() {
-        global $CFG;
         $this->blockname = get_class($this);
         $this->title = get_string('pluginname', $this->blockname);
     }
@@ -109,8 +108,7 @@ class block_navigation extends block_base {
      */
     function get_required_javascript() {
         global $CFG;
-        user_preference_allow_ajax_update('docked_block_instance_'.$this->instance->id, PARAM_INT);
-        $this->page->requires->js_module('core_dock');
+        parent::get_required_javascript();
         $limit = 20;
         if (!empty($CFG->navcourselimit)) {
             $limit = $CFG->navcourselimit;
@@ -127,7 +125,7 @@ class block_navigation extends block_base {
             'expansionlimit' => $expansionlimit
         );
         $this->page->requires->string_for_js('viewallcourses', 'moodle');
-        $this->page->requires->yui_module(array('core_dock', 'moodle-block_navigation-navigation'), 'M.block_navigation.init_add_tree', array($arguments));
+        $this->page->requires->yui_module('moodle-block_navigation-navigation', 'M.block_navigation.init_add_tree', array($arguments));
     }
 
     /**
@@ -136,7 +134,6 @@ class block_navigation extends block_base {
      * @return object $this->content
      */
     function get_content() {
-        global $CFG, $OUTPUT;
         // First check if we have already generated, don't waste cycles
         if ($this->contentgenerated === true) {
             return $this->content;
index e6bc954..5c9aa7d 100644 (file)
Binary files a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js and b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js differ
index b9cd9b2..190c180 100644 (file)
Binary files a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js and b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js differ
index e6bc954..bed94d7 100644 (file)
Binary files a/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js and b/blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js differ
index 2141a95..c7ed370 100644 (file)
@@ -1,3 +1,49 @@
+/**
+ * Navigation block JS.
+ *
+ * This file contains the Navigation block JS..
+ *
+ * @module moodle-block_navigation-navigation
+ */
+
+/**
+ * This namespace will contain all of the contents of the navigation blocks
+ * global navigation and settings.
+ * @namespace M
+ * @class block_navigation
+ * @static
+ */
+M.block_navigation = M.block_navigation || {};
+/**
+ * The number of expandable branches in existence.
+ *
+ * @property expandablebranchcount
+ * @protected
+ * @static
+ */
+M.block_navigation.expandablebranchcount = 1;
+/**
+ * The maximum number of courses to show as part of a branch.
+ *
+ * @property courselimit
+ * @protected
+ * @static
+ */
+M.block_navigation.courselimit = 20;
+/**
+ * Add new instance of navigation tree to tree collection
+ *
+ * @method init_add_tree
+ * @static
+ * @param {Object} properties
+ */
+M.block_navigation.init_add_tree = function(properties) {
+    if (properties.courselimit) {
+        this.courselimit = properties.courselimit;
+    }
+    new TREE(properties);
+};
+
 /**
  * A 'actionkey' Event to help with Y.delegate().
  * The event consists of the left arrow, right arrow, enter and space keys.
  * This event is delegated to branches in the navigation tree.
  * The on() method to subscribe allows specifying the desired trigger actions as JSON.
  *
- * Todo: This could be centralised, a similar Event is defined in blocks/dock.js
+ * @namespace M.block_navigation
+ * @class ActionKey
  */
 Y.Event.define("actionkey", {
-   // Webkit and IE repeat keydown when you hold down arrow keys.
+    // Webkit and IE repeat keydown when you hold down arrow keys.
     // Opera links keypress to page scroll; others keydown.
     // Firefox prevents page scroll via preventDefault() on either
     // keydown or keypress.
     _event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
 
+    /**
+     * The keys to trigger on.
+     * @method _keys
+     */
     _keys: {
         //arrows
         '37': 'collapse',
         '39': 'expand',
-        //(@todo: lrt/rtl/M.core_dock.cfg.orientation decision to assign arrow to meanings)
         '32': 'toggle',
         '13': 'enter'
     },
 
+    /**
+     * Handles key events
+     * @method _keyHandler
+     * @param {EventFacade} e
+     * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers
+     * @param {Object} args
+     */
     _keyHandler: function (e, notifier, args) {
         var actObj;
         if (!args.actions) {
@@ -38,6 +95,13 @@ Y.Event.define("actionkey", {
         }
     },
 
+    /**
+     * Subscribes to events.
+     * @method on
+     * @param {Node} node The node this subscription was applied to.
+     * @param {Subscription} sub The object tracking this subscription.
+     * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers
+     */
     on: function (node, sub, notifier) {
         // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions).
         if (sub.args === null) {
@@ -48,11 +112,23 @@ Y.Event.define("actionkey", {
         }
     },
 
+    /**
+     * Detaches an event listener
+     * @method detach
+     */
     detach: function (node, sub) {
         //detach our _detacher handle of the subscription made in on()
         sub._detacher.detach();
     },
 
+    /**
+     * Creates a delegated event listener.
+     * @method delegate
+     * @param {Node} node The node this subscription was applied to.
+     * @param {Subscription} sub The object tracking this subscription.
+     * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers
+     * @param {String|function} filter Selector string or function that accpets an event object and returns null.
+     */
     delegate: function (node, sub, notifier, filter) {
         // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions).
         if (sub.args === null) {
@@ -63,45 +139,50 @@ Y.Event.define("actionkey", {
         }
     },
 
+    /**
+     * Detaches a delegated event listener.
+     * @method detachDelegate
+     * @param {Node} node The node this subscription was applied to.
+     * @param {Subscription} sub The object tracking this subscription.
+     * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers
+     * @param {String|function} filter Selector string or function that accpets an event object and returns null.
+     */
     detachDelegate: function (node, sub) {
         sub._delegateDetacher.detach();
     }
 });
 
 var EXPANSIONLIMIT_EVERYTHING = 0,
-    //EXPANSIONLIMIT_COURSE     = 20,
-    //EXPANSIONLIMIT_SECTION    = 30,
+    EXPANSIONLIMIT_COURSE     = 20,
+    EXPANSIONLIMIT_SECTION    = 30,
     EXPANSIONLIMIT_ACTIVITY   = 40;
 
-/**
- * Mappings for the different types of nodes coming from the navigation.
- * Copied from lib/navigationlib.php navigation_node constants.
- * @type object
- */
+// Mappings for the different types of nodes coming from the navigation.
+// Copied from lib/navigationlib.php navigation_node constants.
 var NODETYPE = {
-    /** @type int Root node = 0 */
+    // @type int Root node = 0
     ROOTNODE : 0,
-    /** @type int System context = 1 */
+    // @type int System context = 1
     SYSTEM : 1,
-    /** @type int Course category = 10 */
+    // @type int Course category = 10
     CATEGORY : 10,
-    /** @type int MYCATEGORY = 11 */
+    // @type int MYCATEGORY = 11
     MYCATEGORY : 11,
-    /** @type int Course = 20 */
+    // @type int Course = 20
     COURSE : 20,
-    /** @type int Course section = 30 */
+    // @type int Course section = 30
     SECTION : 30,
-    /** @type int Activity (course module) = 40 */
+    // @type int Activity (course module) = 40
     ACTIVITY : 40,
-    /** @type int Resource (course module = 50 */
+    // @type int Resource (course module = 50
     RESOURCE : 50,
-    /** @type int Custom node (could be anything) = 60 */
+    // @type int Custom node (could be anything) = 60
     CUSTOM : 60,
-    /** @type int Setting = 70 */
+    // @type int Setting = 70
     SETTING : 70,
-    /** @type int User context = 80 */
+    // @type int User context = 80
     USER : 80,
-    /** @type int Container = 90 */
+    // @type int Container = 90
     CONTAINER : 90
 };
 
@@ -110,6 +191,11 @@ var NODETYPE = {
  *
  * This class establishes the tree initially, creating expandable branches as
  * required, and delegating the expand/collapse event.
+ *
+ * @namespace M.block_navigation
+ * @class Tree
+ * @constructor
+ * @extends Y.Base
  */
 var TREE = function() {
     TREE.superclass.constructor.apply(this, arguments);
@@ -117,17 +203,27 @@ var TREE = function() {
 TREE.prototype = {
     /**
      * The tree's ID, normally its block instance id.
+     * @property id
+     * @type Int
+     * @protected
      */
     id : null,
     /**
      * An array of initialised branches.
+     * @property branches
+     * @type Array
+     * @protected
      */
     branches : [],
     /**
      * Initialise the tree object when its first created.
+     * @method initializer
+     * @param {Object} config
      */
     initializer : function(config) {
-        this.id = config.id;
+        Y.log('Initialising navigation block tree', 'note', 'moodle-block_navigation');
+
+        this.id = parseInt(config.id, 10);
 
         var node = Y.one('#inst'+config.id);
 
@@ -166,14 +262,11 @@ TREE.prototype = {
             Y.delegate('click', this.fire_branch_action, node.one('.block_tree'), '.tree_item.branch[data-expandable]', this);
             Y.delegate('actionkey', this.fire_branch_action, node.one('.block_tree'), '.tree_item.branch[data-expandable]', this);
         }
-
-        // Call the generic blocks init method to add all the generic stuff
-        if (this.get('candock')) {
-            this.initialise_block(Y, node);
-        }
     },
     /**
      * Fire actions for a branch when an event occurs.
+     * @method fire_branch_action
+     * @param {EventFacade} event
      */
     fire_branch_action : function(event) {
         var id = event.currentTarget.getAttribute('id');
@@ -183,6 +276,9 @@ TREE.prototype = {
     /**
      * This is a callback function responsible for expanding and collapsing the
      * branches of the tree. It is delegated to rather than multiple event handles.
+     * @method toggleExpansion
+     * @param {EventFacade} e
+     * @return Boolean
      */
     toggleExpansion : function(e) {
         // First check if they managed to click on the li iteslf, then find the closest
@@ -238,46 +334,66 @@ TREE.prototype = {
 
         // If this block can dock tell the dock to resize if required and check
         // the width on the dock panel in case it is presently in use.
-        if (this.get('candock')) {
-            M.core_dock.resize();
-            var panel = M.core_dock.getPanel();
-            if (panel.visible) {
-                panel.correctWidth();
-            }
+        if (this.get('candock') && M.core.dock.notifyBlockChange) {
+            M.core.dock.notifyBlockChange(this.id);
         }
+        return true;
+
     }
 };
 // The tree extends the YUI base foundation.
 Y.extend(TREE, Y.Base, TREE.prototype, {
     NAME : 'navigation-tree',
     ATTRS : {
-        instance : {
-            value : null
-        },
+        /**
+         * True if the block can dock.
+         * @attribute candock
+         * @type Boolean
+         */
         candock : {
             validator : Y.Lang.isBool,
             value : false
         },
+        /**
+         * If set to true nodes will be opened/closed in an accordian fashion.
+         * @attribute accordian
+         * @type Boolean
+         */
         accordian : {
             validator : Y.Lang.isBool,
             value : false
         },
+        /**
+         * The nodes that get shown.
+         * @attribute expansionlimit
+         * @type Integer
+         */
         expansionlimit : {
             value : 0,
             setter : function(val) {
-                return parseInt(val, 10);
+                val = parseInt(val, 10);
+                if (val !== EXPANSIONLIMIT_EVERYTHING &&
+                    val !== EXPANSIONLIMIT_COURSE &&
+                    val !== EXPANSIONLIMIT_SECTION &&
+                    val !== EXPANSIONLIMIT_ACTIVITY) {
+                    val = EXPANSIONLIMIT_EVERYTHING;
+                }
+                return val;
             }
         }
     }
 });
-if (M.core_dock && M.core_dock.genericblock) {
-    Y.augment(TREE, M.core_dock.genericblock);
-}
 
 /**
- * The tree branch class.
+ * The Branch class.
+ *
  * This class is used to manage a tree branch, in particular its ability to load
  * its contents by AJAX.
+ *
+ * @namespace M.block_navigation
+ * @class Branch
+ * @constructor
+ * @extends Y.Base
  */
 BRANCH = function() {
     BRANCH.superclass.constructor.apply(this, arguments);
@@ -285,10 +401,15 @@ BRANCH = function() {
 BRANCH.prototype = {
     /**
      * The node for this branch (p)
+     * @property node
+     * @type Node
+     * @protected
      */
     node : null,
     /**
      * Initialises the branch when it is first created.
+     * @method initializer
+     * @param {Object} config
      */
     initializer : function(config) {
         var i,
@@ -308,8 +429,7 @@ BRANCH.prototype = {
             }
         }
         // Get the node for this branch
-        this.node = Y.one('#', this.get('id'));
-        // Now check whether the branch is not expandable because of the expansionlimit
+        this.node = Y.one('#'+this.get('id'));
         var expansionlimit = this.get('tree').get('expansionlimit');
         var type = this.get('type');
         if (expansionlimit !== EXPANSIONLIMIT_EVERYTHING &&  type >= expansionlimit && type <= EXPANSIONLIMIT_ACTIVITY) {
@@ -322,6 +442,11 @@ BRANCH.prototype = {
      *
      * This function creates a DOM structure for the branch and then injects
      * it into the navigation tree at the correct point.
+     *
+     * @method draw
+     * @chainable
+     * @param {Node} element
+     * @return Branch
      */
     draw : function(element) {
 
@@ -343,7 +468,7 @@ BRANCH.prototype = {
         // Prepare the icon, should be an object representing a pix_icon
         var branchicon = false;
         var icon = this.get('icon');
-        if (icon && (!isbranch || this.get('type') == NODETYPE.ACTIVITY)) {
+        if (icon && (!isbranch || this.get('type') === NODETYPE.ACTIVITY)) {
             branchicon = Y.Node.create('<img alt="" />');
             branchicon.setAttribute('src', M.util.image_url(icon.pix, icon.component));
             branchli.addClass('item_with_icon');
@@ -407,6 +532,8 @@ BRANCH.prototype = {
     },
     /**
      * Gets the UL element that children for this branch should be inserted into.
+     * @method getChildrenUL
+     * @return Node
      */
     getChildrenUL : function() {
         var ul = this.node.next('ul');
@@ -421,6 +548,10 @@ BRANCH.prototype = {
      *
      * This function calls ajaxProcessResponse with the result of the AJAX
      * request made here.
+     *
+     * @method ajaxLoad
+     * @param {EventFacade} e
+     * @return Bool
      */
     ajaxLoad : function(e) {
         if (e.type === 'actionkey' && e.action !== 'enter') {
@@ -444,6 +575,7 @@ BRANCH.prototype = {
             // We've already loaded this stuff.
             return true;
         }
+        Y.log('Loading navigation branch via AJAX: '+this.get('key'), 'note', 'moodle-block_navigation');
         this.node.addClass('loadingbranch');
 
         var params = {
@@ -467,6 +599,11 @@ BRANCH.prototype = {
     /**
      * Processes an AJAX request to load the content of this branch through
      * AJAX.
+     *
+     * @method ajaxProcessResponse
+     * @param {Int} tid The transaction id.
+     * @param {Object} outcome
+     * @return Boolean
      */
     ajaxProcessResponse : function(tid, outcome) {
         this.node.removeClass('loadingbranch');
@@ -477,20 +614,28 @@ BRANCH.prototype = {
                 var coursecount = 0;
                 for (var i in object.children) {
                     if (typeof(object.children[i])==='object') {
-                        if (object.children[i].type == NODETYPE.COURSE) {
+                        if (object.children[i].type === NODETYPE.COURSE) {
                             coursecount++;
                         }
                         this.addChild(object.children[i]);
                     }
                 }
-                if ((this.get('type') == NODETYPE.CATEGORY || this.get('type') == NODETYPE.ROOTNODE || this.get('type') == NODETYPE.MYCATEGORY)
+                if ((this.get('type') === NODETYPE.CATEGORY || this.get('type') === NODETYPE.ROOTNODE || this.get('type') === NODETYPE.MYCATEGORY)
                     && coursecount >= M.block_navigation.courselimit) {
                     this.addViewAllCoursesChild(this);
                 }
+                Y.log('AJAX loading complete.', 'note', 'moodle-block_navigation');
+                // If this block can dock tell the dock to resize if required and check
+                // the width on the dock panel in case it is presently in use.
+                if (this.get('tree').get('candock') && M.core.dock.notifyBlockChange) {
+                    M.core.dock.notifyBlockChange(this.get('tree').id);
+                }
                 return true;
             }
+            Y.log('AJAX loading complete but there were no children.', 'note', 'moodle-block_navigation');
         } catch (ex) {
-            // If we got here then there was an error parsing the result
+            // If we got here then there was an error parsing the result.
+            Y.log('Error parsing AJAX response or adding branches to the navigation tree', 'error', 'moodle-block_navigation');
         }
         // The branch is empty so class it accordingly
         this.node.replaceClass('branch', 'emptybranch');
@@ -499,6 +644,10 @@ BRANCH.prototype = {
     /**
      * Turns the branch object passed to the method into a proper branch object
      * and then adds it as a child of this branch.
+     *
+     * @method addChild
+     * @param {Object} branchobj
+     * @return Boolean
      */
     addChild : function(branchobj) {
         // Make the new branch into an object
@@ -509,14 +658,14 @@ BRANCH.prototype = {
             var count = 0, i, children = branch.get('children');
             for (i in children) {
                 // Add each branch to the tree
-                if (children[i].type == NODETYPE.COURSE) {
+                if (children[i].type === NODETYPE.COURSE) {
                     count++;
                 }
                 if (typeof(children[i]) === 'object') {
                     branch.addChild(children[i]);
                 }
             }
-            if ((branch.get('type') == NODETYPE.CATEGORY || branch.get('type') == NODETYPE.MYCATEGORY)
+            if ((branch.get('type') === NODETYPE.CATEGORY || branch.get('type') === NODETYPE.MYCATEGORY)
                 && count >= M.block_navigation.courselimit) {
                 this.addViewAllCoursesChild(branch);
             }
@@ -526,10 +675,13 @@ BRANCH.prototype = {
 
     /**
      * Add a link to view all courses in a category
+     *
+     * @method addViewAllCoursesChild
+     * @param {BRANCH} branch
      */
     addViewAllCoursesChild: function(branch) {
         var url = null;
-        if (branch.get('type') == NODETYPE.ROOTNODE) {
+        if (branch.get('type') === NODETYPE.ROOTNODE) {
             if (branch.get('key') === 'mycourses') {
                 url = M.cfg.wwwroot + '/my';
             } else {
@@ -550,9 +702,22 @@ BRANCH.prototype = {
 Y.extend(BRANCH, Y.Base, BRANCH.prototype, {
     NAME : 'navigation-branch',
     ATTRS : {
+        /**
+         * The Tree this branch belongs to.
+         * @attribute tree
+         * @type TREE
+         * @required
+         * @writeOnce
+         */
         tree : {
+            writeOnce : 'initOnly',
             validator : Y.Lang.isObject
         },
+        /**
+         * The name of this branch.
+         * @attribute name
+         * @type String
+         */
         name : {
             value : '',
             validator : Y.Lang.isString,
@@ -560,10 +725,21 @@ Y.extend(BRANCH, Y.Base, BRANCH.prototype, {
                 return val.replace(/\n/g, '<br />');
             }
         },
+        /**
+         * The title to use for this branch.
+         * @attribute title
+         * @type String
+         */
         title : {
             value : '',
             validator : Y.Lang.isString
         },
+        /**
+         * The ID of this branch.
+         * The ID and Type should always form a unique pair.
+         * @attribute id
+         * @type String
+         */
         id : {
             value : '',
             validator : Y.Lang.isString,