Merge branch 'MDL-30839-master' of git://github.com/phalacee/moodle
authorDan Poltawski <dan@moodle.com>
Thu, 22 Aug 2013 03:17:49 +0000 (11:17 +0800)
committerDan Poltawski <dan@moodle.com>
Thu, 22 Aug 2013 03:17:49 +0000 (11:17 +0800)
228 files changed:
admin/cli/automated_backups.php
admin/cli/install.php
admin/qbehaviours.php
admin/qtypes.php
admin/tool/generator/classes/backend.php [new file with mode: 0644]
admin/tool/generator/classes/make_form.php [new file with mode: 0644]
admin/tool/generator/cli/generate.php
admin/tool/generator/cli/maketestcourse.php [new file with mode: 0644]
admin/tool/generator/index.php
admin/tool/generator/lang/en/tool_generator.php
admin/tool/generator/maketestcourse.php [new file with mode: 0644]
admin/tool/generator/settings.php [new file with mode: 0644]
admin/tool/generator/tests/maketestcourse_test.php [new file with mode: 0644]
admin/tool/generator/version.php
admin/tool/phpunit/webrunner.php
admin/tool/uploadcourse/classes/helper.php
admin/tool/uploadcourse/classes/processor.php
admin/tool/uploadcourse/classes/step2_form.php
admin/tool/uploadcourse/index.php
admin/tool/uploadcourse/tests/course_test.php
admin/tool/uploadcourse/tests/helper_test.php
auth/cas/cli/sync_users.php
auth/ldap/auth.php
auth/ldap/cli/sync_users.php
auth/ldap/ntlmsso_magic.php
backup/import.php
backup/moodle2/restore_qtype_plugin.class.php
backup/util/factories/backup_factory.class.php
backup/util/factories/tests/factories_test.php
backup/util/plan/restore_plan.class.php
badges/lib/awardlib.php
badges/renderer.php
blocks/moodleblock.class.php
blocks/tags/block_tags.php
cache/stores/file/lib.php
cache/tests/administration_helper_test.php
calendar/lib.php
calendar/tests/lib_test.php [new file with mode: 0644]
cohort/lib.php
cohort/tests/cohortlib_test.php
config-dist.php
course/completion.php
course/delete.php
course/editsection.php
course/format/renderer.php
course/lib.php
course/loginas.php
course/manage.php
course/tests/courselib_test.php
course/view.php
course/yui/dragdrop/dragdrop.js
enrol/flatfile/lib.php
enrol/ldap/cli/sync.php
enrol/tests/enrollib_test.php
enrol/yui/rolemanager/assets/skins/sam/rolemanager.css
enrol/yui/rolemanager/rolemanager.js
files/externallib.php
files/tests/externallib_test.php
grade/grading/form/rubric/styles.css
grade/report/grader/lib.php
group/index.php
install.php
lang/en/auth.php
lang/en/cohort.php
lang/en/completion.php
lang/en/moodle.php
lib/accesslib.php
lib/adminlib.php
lib/ajax/ajaxlib.php
lib/ajax/tests/ajaxlib_test.php [deleted file]
lib/badgeslib.php
lib/classes/component.php
lib/classes/event/assessable_submitted.php
lib/classes/event/assessable_uploaded.php
lib/classes/event/base.php
lib/classes/event/blog_entry_created.php
lib/classes/event/blog_entry_deleted.php
lib/classes/event/cohort_created.php [new file with mode: 0644]
lib/classes/event/cohort_deleted.php [new file with mode: 0644]
lib/classes/event/cohort_member_added.php [new file with mode: 0644]
lib/classes/event/cohort_member_removed.php [new file with mode: 0644]
lib/classes/event/cohort_updated.php [new file with mode: 0644]
lib/classes/event/course_category_deleted.php [new file with mode: 0644]
lib/classes/event/course_completed.php
lib/classes/event/course_completion_updated.php [new file with mode: 0644]
lib/classes/event/course_content_deleted.php [new file with mode: 0644]
lib/classes/event/course_created.php [new file with mode: 0644]
lib/classes/event/course_deleted.php [new file with mode: 0644]
lib/classes/event/course_module_completion_updated.php
lib/classes/event/course_restored.php [new file with mode: 0644]
lib/classes/event/course_section_updated.php [new file with mode: 0644]
lib/classes/event/course_updated.php [new file with mode: 0644]
lib/classes/event/role_allow_assign_updated.php
lib/classes/event/role_allow_override_updated.php
lib/classes/event/role_allow_switch_updated.php
lib/classes/event/role_assigned.php
lib/classes/event/role_capabilities_updated.php
lib/classes/event/role_deleted.php
lib/classes/event/role_unassigned.php
lib/classes/event/user_loggedin.php
lib/classes/event/user_loggedinas.php [new file with mode: 0644]
lib/classes/useragent.php [new file with mode: 0644]
lib/coursecatlib.php
lib/cronlib.php
lib/csslib.php
lib/deprecatedlib.php
lib/dml/mssql_native_moodle_database.php
lib/dml/pdo_moodle_database.php
lib/dml/sqlsrv_native_moodle_database.php
lib/dml/tests/dml_test.php
lib/editor/tinymce/classes/plugin.php
lib/editor/tinymce/cli/update_lang_files.php
lib/editor/tinymce/lib.php
lib/editor/tinymce/plugins/spellchecker/lib.php
lib/enrollib.php
lib/excellib.class.php
lib/filelib.php
lib/filestorage/file_storage.php
lib/formslib.php
lib/installlib.php
lib/medialib.php
lib/minify/config.php
lib/modinfolib.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputlib.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/pagelib.php
lib/phpmailer/moodle_phpmailer.php
lib/phpunit/bootstrap.php
lib/phpunit/classes/advanced_testcase.php
lib/phpunit/classes/phpmailer_sink.php [new file with mode: 0644]
lib/phpunit/classes/util.php
lib/phpunit/lib.php
lib/phpunit/tests/advanced_test.php
lib/pluginlib.php
lib/sessionlib.php
lib/setup.php
lib/setuplib.php
lib/simplepie/moodle_simplepie.php
lib/tests/completionlib_test.php
lib/tests/csslib_test.php
lib/tests/environment_test.php
lib/tests/event_test.php
lib/tests/fixtures/event_fixtures.php
lib/tests/medialib_test.php
lib/tests/moodlelib_test.php
lib/tests/sessionlib_test.php [new file with mode: 0644]
lib/tests/statslib_test.php
lib/tests/theme_config_test.php
lib/tests/useragent_test.php [new file with mode: 0644]
lib/tests/weblib_test.php
lib/upgrade.txt
lib/upgradelib.php
lib/weblib.php
lib/yui/build/moodle-core-blocks/moodle-core-blocks-debug.js
lib/yui/build/moodle-core-blocks/moodle-core-blocks-min.js
lib/yui/build/moodle-core-blocks/moodle-core-blocks.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js
lib/yui/dragdrop/dragdrop.js
lib/yui/src/blocks/js/blocks.js
lib/yui/src/notification/js/dialogue.js
mod/assign/tests/generator/lib.php
mod/assign/tests/lib_test.php
mod/chat/gui_ajax/index.php
mod/chat/gui_ajax/module.js
mod/chat/gui_ajax/theme/bubble/chat.css
mod/chat/gui_ajax/theme/bubble/config.php
mod/chat/styles.css
mod/forum/classes/post_form.php [new file with mode: 0644]
mod/forum/lib.php
mod/forum/post.php
mod/forum/post_form.php
mod/forum/tests/lib_test.php
mod/forum/upgrade.txt
mod/lti/locallib.php
mod/quiz/renderer.php
mod/scorm/datamodels/aicclib.php
mod/scorm/db/upgrade.php
mod/scorm/lang/en/scorm.php
mod/scorm/lib.php
mod/scorm/locallib.php
mod/scorm/mod_form.php
mod/scorm/tests/packages/badscorm.zip [new file with mode: 0644]
mod/scorm/tests/packages/invalid.zip [new file with mode: 0644]
mod/scorm/tests/packages/validaicc.zip [new file with mode: 0644]
mod/scorm/tests/packages/validscorm.zip [new file with mode: 0644]
mod/scorm/tests/validatepackage_test.php [new file with mode: 0644]
mod/scorm/version.php
mod/workshop/renderer.php
question/behaviour/manualgraded/tests/walkthrough_test.php
question/category.php
question/category_class.php
question/category_form.php
question/engine/datalib.php
question/engine/questionattempt.php
question/engine/questionattemptstep.php
question/type/essay/question.php
question/type/essay/tests/helper.php
question/type/essay/tests/question_test.php
question/type/essay/tests/walkthrough_test.php
question/type/match/db/upgrade.php
question/type/multianswer/module.js
question/type/multianswer/styles.css
question/type/numerical/edit_numerical_form.php
question/type/numerical/styles.css
report/performance/locallib.php
repository/skydrive/lib.php
tag/coursetags_more.php
tag/coursetagslib.php
tag/locallib.php
tag/upgrade.txt
theme/base/style/admin.css
theme/base/style/core.css
theme/base/style/course.css
theme/base/style/filemanager.css
theme/bootstrapbase/config.php
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/filemanager.less
theme/bootstrapbase/less/moodle/question.less
theme/bootstrapbase/style/moodle.css
theme/index.php
theme/mymobile/config.php
theme/switchdevice.php
version.php

index dc80c7a..3c3841e 100644 (file)
@@ -75,8 +75,7 @@ if (!empty($CFG->showcronsql)) {
     $DB->set_debug(true);
 }
 if (!empty($CFG->showcrondebugging)) {
-    $CFG->debug = DEBUG_DEVELOPER;
-    $CFG->debugdisplay = true;
+    set_debugging(DEBUG_DEVELOPER, true);
 }
 
 $starttime = microtime();
index 21617cf..fcd25b2 100644 (file)
@@ -162,6 +162,9 @@ $CFG->running_installer    = true;
 $CFG->early_install_lang   = true;
 $CFG->ostype               = (stristr(PHP_OS, 'win') && !stristr(PHP_OS, 'darwin')) ? 'WINDOWS' : 'UNIX';
 $CFG->dboptions            = array();
+$CFG->debug                = (E_ALL | E_STRICT);
+$CFG->debugdisplay         = true;
+$CFG->debugdeveloper       = true;
 
 $parts = explode('/', str_replace('\\', '/', dirname(dirname(__FILE__))));
 $CFG->admin                = array_pop($parts);
index cb59976..85f0c8e 100644 (file)
@@ -143,7 +143,7 @@ if (($delete = optional_param('delete', '', PARAM_PLUGIN)) && confirm_sesskey())
         print_error('cannotdeletemissingbehaviour', 'question', $thispageurl);
     }
 
-    if (!isset($behaviours[$delete])) {
+    if (!isset($behaviours[$delete]) && !get_config('qbehaviour_' . $delete, 'version')) {
         print_error('unknownbehaviour', 'question', $thispageurl, $delete);
     }
 
@@ -171,10 +171,7 @@ if (($delete = optional_param('delete', '', PARAM_PLUGIN)) && confirm_sesskey())
     echo $OUTPUT->header();
     echo $OUTPUT->heading(get_string('deletingbehaviour', 'question', $behaviourname));
 
-    // Delete any configuration records.
-    if (!unset_all_config_for_plugin('qbehaviour_' . $delete)) {
-        echo $OUTPUT->notification(get_string('errordeletingconfig', 'admin', 'qbehaviour_' . $delete));
-    }
+    // Remove this behaviour from configurations where it might appear.
     if (($key = array_search($delete, $disabledbehaviours)) !== false) {
         unset($disabledbehaviours[$key]);
         set_config('disabledbehaviours', implode(',', $disabledbehaviours), 'question');
@@ -185,12 +182,10 @@ if (($delete = optional_param('delete', '', PARAM_PLUGIN)) && confirm_sesskey())
         set_config('behavioursortorder', implode(',', $behaviourorder), 'question');
     }
 
-    // Then the tables themselves
-    drop_plugin_tables($delete, core_component::get_plugin_directory('qbehaviour', $delete) . '/db/install.xml', false);
-
-    // Remove event handlers and dequeue pending events
-    events_uninstall('qbehaviour_' . $delete);
+    // Then uninstall the plugin.
+    uninstall_plugin('qbehaviour', $delete);
 
+    // Display a message.
     $a = new stdClass();
     $a->behaviour = $behaviourname;
     $a->directory = core_component::get_plugin_directory('qbehaviour', $delete);
index e12d3cc..3e865d1 100644 (file)
@@ -129,7 +129,7 @@ if (($delete = optional_param('delete', '', PARAM_PLUGIN)) && confirm_sesskey())
         print_error('cannotdeletemissingqtype', 'question', $thispageurl);
     }
 
-    if (!isset($qtypes[$delete])) {
+    if (!isset($qtypes[$delete]) && !get_config('qtype_' . $delete, 'version')) {
         print_error('unknownquestiontype', 'question', $thispageurl, $delete);
     }
 
@@ -158,18 +158,12 @@ if (($delete = optional_param('delete', '', PARAM_PLUGIN)) && confirm_sesskey())
     echo $OUTPUT->header();
     echo $OUTPUT->heading(get_string('deletingqtype', 'question', $qtypename));
 
-    // Delete any configuration records.
-    if (!unset_all_config_for_plugin('qtype_' . $delete)) {
-        echo $OUTPUT->notification(get_string('errordeletingconfig', 'admin', 'qtype_' . $delete));
-    }
+    // Delete any questoin configuration records mentioning this plugin.
     unset_config($delete . '_disabled', 'question');
     unset_config($delete . '_sortorder', 'question');
 
-    // Then the tables themselves
-    drop_plugin_tables($delete, $qtypes[$delete]->plugin_dir() . '/db/install.xml', false);
-
-    // Remove event handlers and dequeue pending events
-    events_uninstall('qtype_' . $delete);
+    // Then uninstall the plugin.
+    uninstall_plugin('qtype', $delete);
 
     $a = new stdClass();
     $a->qtype = $qtypename;
diff --git a/admin/tool/generator/classes/backend.php b/admin/tool/generator/classes/backend.php
new file mode 100644 (file)
index 0000000..beed421
--- /dev/null
@@ -0,0 +1,606 @@
+<?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/>.
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Backend code for the 'make large course' tool.
+ *
+ * @package tool_generator
+ * @copyright 2013 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_generator_backend {
+    /**
+     * @var int Lowest (smallest) size index
+     */
+    const MIN_SIZE = 0;
+    /**
+     * @var int Highest (largest) size index
+     */
+    const MAX_SIZE = 5;
+    /**
+     * @var int Default size index
+     */
+    const DEFAULT_SIZE = 3;
+
+    /**
+     * @var array Number of sections in course
+     */
+    private static $paramsections = array(1, 10, 100, 500, 1000, 2000);
+    /**
+     * @var array Number of Page activities in course
+     */
+    private static $parampages = array(1, 50, 200, 1000, 5000, 10000);
+    /**
+     * @var array Number of students enrolled in course
+     */
+    private static $paramusers = array(1, 100, 1000, 10000, 50000, 100000);
+    /**
+     * Total size of small files: 1KB, 1MB, 10MB, 100MB, 1GB, 2GB.
+     *
+     * @var array Number of small files created in a single file activity
+     */
+    private static $paramsmallfilecount = array(1, 64, 128, 1024, 16384, 32768);
+    /**
+     * @var array Size of small files (to make the totals into nice numbers)
+     */
+    private static $paramsmallfilesize = array(1024, 16384, 81920, 102400, 65536, 65536);
+    /**
+     * Total size of big files: 8KB, 8MB, 80MB, 800MB, 8GB, 16GB.
+     *
+     * @var array Number of big files created as individual file activities
+     */
+    private static $parambigfilecount = array(1, 2, 5, 10, 10, 10);
+    /**
+     * @var array Size of each large file
+     */
+    private static $parambigfilesize = array(8192, 4194304, 16777216, 83886080,
+            858993459, 1717986918);
+    /**
+     * @var array Number of forum discussions
+     */
+    private static $paramforumdiscussions = array(1, 10, 100, 500, 1000, 2000);
+    /**
+     * @var array Number of forum posts per discussion
+     */
+    private static $paramforumposts = array(2, 2, 5, 10, 10, 10);
+
+    /**
+     * @var string Course shortname
+     */
+    private $shortname;
+
+    /**
+     * @var int Size code (index in the above arrays)
+     */
+    private $size;
+
+    /**
+     * @var bool True if displaying progress
+     */
+    private $progress;
+
+    /**
+     * @var testing_data_generator Data generator
+     */
+    private $generator;
+
+    /**
+     * @var stdClass Course object
+     */
+    private $course;
+
+    /**
+     * @var int Epoch time at which last dot was displayed
+     */
+    private $lastdot;
+
+    /**
+     * @var int Epoch time at which last percentage was displayed
+     */
+    private $lastpercentage;
+
+    /**
+     * @var int Epoch time at which current step (current set of dots) started
+     */
+    private $starttime;
+
+    /**
+     * @var array Array from test user number (1...N) to userid in database
+     */
+    private $userids;
+
+    /**
+     * Constructs object ready to create course.
+     *
+     * @param string $shortname Course shortname
+     * @param int $size Size as numeric index
+     * @param bool $progress True if progress information should be displayed
+     * @return int Course id
+     * @throws coding_exception If parameters are invalid
+     */
+    public function __construct($shortname, $size, $progress = true) {
+        // Check parameter.
+        if ($size < self::MIN_SIZE || $size > self::MAX_SIZE) {
+            throw new coding_exception('Invalid size');
+        }
+
+        // Set parameters.
+        $this->shortname = $shortname;
+        $this->size = $size;
+        $this->progress = $progress;
+    }
+
+    /**
+     * Gets a list of size choices supported by this backend.
+     *
+     * @return array List of size (int) => text description for display
+     */
+    public static function get_size_choices() {
+        $options = array();
+        for ($size = self::MIN_SIZE; $size <= self::MAX_SIZE; $size++) {
+            $options[$size] = get_string('size_' . $size, 'tool_generator');
+        }
+        return $options;
+    }
+
+    /**
+     * Converts a size name into the numeric constant.
+     *
+     * @param string $sizename Size name e.g. 'L'
+     * @return int Numeric version
+     * @throws coding_exception If the size name is not known
+     */
+    public static function size_for_name($sizename) {
+        for ($size = self::MIN_SIZE; $size <= self::MAX_SIZE; $size++) {
+            if ($sizename == get_string('shortsize_' . $size, 'tool_generator')) {
+                return $size;
+            }
+        }
+        throw new coding_exception("Unknown size name '$sizename'");
+    }
+
+    /**
+     * Checks that a shortname is available (unused).
+     *
+     * @param string $shortname Proposed course shortname
+     * @return string An error message if the name is unavailable or '' if OK
+     */
+    public static function check_shortname_available($shortname) {
+        global $DB;
+        $fullname = $DB->get_field('course', 'fullname',
+                array('shortname' => $shortname), IGNORE_MISSING);
+        if ($fullname !== false) {
+            // I wanted to throw an exception here but it is not possible to
+            // use strings from moodle.php in exceptions, and I didn't want
+            // to duplicate the string in tool_generator, so I changed this to
+            // not use exceptions.
+            return get_string('shortnametaken', 'moodle', $fullname);
+        }
+        return '';
+    }
+
+    /**
+     * Runs the entire 'make' process.
+     *
+     * @return int Course id
+     */
+    public function make() {
+        global $DB, $CFG;
+        require_once($CFG->dirroot . '/lib/phpunit/classes/util.php');
+
+        raise_memory_limit(MEMORY_EXTRA);
+
+        if ($this->progress && !CLI_SCRIPT) {
+            echo html_writer::start_tag('ul');
+        }
+
+        $entirestart = microtime(true);
+
+        // Start transaction.
+        $transaction = $DB->start_delegated_transaction();
+
+        // Get generator.
+        $this->generator = phpunit_util::get_data_generator();
+
+        // Make course.
+        $this->course = $this->create_course();
+        $this->create_users();
+        $this->create_pages();
+        $this->create_small_files();
+        $this->create_big_files();
+        $this->create_forum();
+
+        // Log total time.
+        $this->log('complete', round(microtime(true) - $entirestart, 1));
+
+        if ($this->progress && !CLI_SCRIPT) {
+            echo html_writer::end_tag('ul');
+        }
+
+        // Commit transaction and finish.
+        $transaction->allow_commit();
+        return $this->course->id;
+    }
+
+    /**
+     * Creates the actual course.
+     *
+     * @return stdClass Course record
+     */
+    private function create_course() {
+        $this->log('createcourse', $this->shortname);
+        $courserecord = array('shortname' => $this->shortname,
+                'fullname' => get_string('fullname', 'tool_generator',
+                    array('size' => get_string('shortsize_' . $this->size, 'tool_generator'))),
+                'numsections' => self::$paramsections[$this->size]);
+        return $this->generator->create_course($courserecord, array('createsections' => true));
+    }
+
+    /**
+     * Creates a number of user accounts and enrols them on the course.
+     * Note: Existing user accounts that were created by this system are
+     * reused if available.
+     */
+    private function create_users() {
+        global $DB;
+
+        // Work out total number of users.
+        $count = self::$paramusers[$this->size];
+
+        // Get existing users in order. We will 'fill up holes' in this up to
+        // the required number.
+        $this->log('checkaccounts', $count);
+        $nextnumber = 1;
+        $rs = $DB->get_recordset_select('user', $DB->sql_like('username', '?'),
+                array('tool_generator_%'), 'username', 'id, username');
+        foreach ($rs as $rec) {
+            // Extract number from username.
+            $matches = array();
+            if (!preg_match('~^tool_generator_([0-9]{6})$~', $rec->username, $matches)) {
+                continue;
+            }
+            $number = (int)$matches[1];
+
+            // Create missing users in range up to this.
+            if ($number != $nextnumber) {
+                $this->create_user_accounts($nextnumber, min($number - 1, $count));
+            } else {
+                $this->userids[$number] = (int)$rec->id;
+            }
+
+            // Stop if we've got enough users.
+            $nextnumber = $number + 1;
+            if ($number >= $count) {
+                break;
+            }
+        }
+        $rs->close();
+
+        // Create users from end of existing range.
+        if ($nextnumber <= $count) {
+            $this->create_user_accounts($nextnumber, $count);
+        }
+
+        // Assign all users to course.
+        $this->log('enrol', $count, true);
+
+        $enrolplugin = enrol_get_plugin('manual');
+        $instances = enrol_get_instances($this->course->id, true);
+        foreach ($instances as $instance) {
+            if ($instance->enrol === 'manual') {
+                break;
+            }
+        }
+        if ($instance->enrol !== 'manual') {
+            throw new coding_exception('No manual enrol plugin in course');
+        }
+        $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
+
+        for ($number = 1; $number <= $count; $number++) {
+            // Enrol user.
+            $enrolplugin->enrol_user($instance, $this->userids[$number], $role->id);
+            $this->dot($number, $count);
+        }
+
+        $this->end_log();
+    }
+
+    /**
+     * Creates user accounts with a numeric range.
+     *
+     * @param int $first Number of first user
+     * @param int $last Number of last user
+     */
+    private function create_user_accounts($first, $last) {
+        $this->log('createaccounts', (object)array('from' => $first, 'to' => $last), true);
+        $count = $last - $first + 1;
+        $done = 0;
+        for ($number = $first; $number <= $last; $number++, $done++) {
+            // Work out username with 6-digit number.
+            $textnumber = (string)$number;
+            while (strlen($textnumber) < 6) {
+                $textnumber = '0' . $textnumber;
+            }
+            $username = 'tool_generator_' . $textnumber;
+
+            // Create user account.
+            $record = array('firstname' => get_string('firstname', 'tool_generator'),
+                    'lastname' => $number, 'username' => $username);
+            $user = $this->generator->create_user($record);
+            $this->userids[$number] = (int)$user->id;
+            $this->dot($done, $count);
+        }
+        $this->end_log();
+    }
+
+    /**
+     * Creates a number of Page activities.
+     */
+    private function create_pages() {
+        // Set up generator.
+        $pagegenerator = $this->generator->get_plugin_generator('mod_page');
+
+        // Create pages.
+        $number = self::$parampages[$this->size];
+        $this->log('createpages', $number, true);
+        for ($i=0; $i<$number; $i++) {
+            $record = array('course' => $this->course->id);
+            $options = array('section' => $this->get_random_section());
+            $pagegenerator->create_instance($record, $options);
+            $this->dot($i, $number);
+        }
+
+        $this->end_log();
+    }
+
+    /**
+     * Creates one resource activity with a lot of small files.
+     */
+    private function create_small_files() {
+        $count = self::$paramsmallfilecount[$this->size];
+        $this->log('createsmallfiles', $count, true);
+
+        // Create resource with default textfile only.
+        $resourcegenerator = $this->generator->get_plugin_generator('mod_resource');
+        $record = array('course' => $this->course->id,
+                'name' => get_string('smallfiles', 'tool_generator'));
+        $options = array('section' => 0);
+        $resource = $resourcegenerator->create_instance($record, $options);
+
+        // Add files.
+        $fs = get_file_storage();
+        $context = context_module::instance($resource->cmid);
+        $filerecord = array('component' => 'mod_resource', 'filearea' => 'content',
+                'contextid' => $context->id, 'itemid' => 0, 'filepath' => '/');
+        for ($i = 0; $i < $count; $i++) {
+            $filerecord['filename'] = 'smallfile' . $i . '.dat';
+
+            // Generate random binary data (different for each file so it
+            // doesn't compress unrealistically).
+            $data = self::get_random_binary(self::$paramsmallfilesize[$this->size]);
+
+            $fs->create_file_from_string($filerecord, $data);
+            $this->dot($i, $count);
+        }
+
+        $this->end_log();
+    }
+
+    /**
+     * Creates a string of random binary data. The start of the string includes
+     * the current time, in an attempt to avoid large-scale repetition.
+     *
+     * @param int $length Number of bytes
+     * @return Random data
+     */
+    private static function get_random_binary($length) {
+        $data = microtime(true);
+        if (strlen($data) > $length) {
+            // Use last digits of data.
+            return substr($data, -$length);
+        }
+        $length -= strlen($data);
+        for ($j=0; $j < $length; $j++) {
+            $data .= chr(rand(1, 255));
+        }
+        return $data;
+    }
+
+    /**
+     * Creates a number of resource activities with one big file each.
+     */
+    private function create_big_files() {
+        global $CFG;
+
+        // Work out how many files and how many blocks to use (up to 64KB).
+        $count = self::$parambigfilecount[$this->size];
+        $blocks = ceil(self::$parambigfilesize[$this->size] / 65536);
+        $blocksize = floor(self::$parambigfilesize[$this->size] / $blocks);
+
+        $this->log('createbigfiles', $count, true);
+
+        // Prepare temp area.
+        $tempfolder = make_temp_directory('tool_generator');
+        $tempfile = $tempfolder . '/' . rand();
+
+        // Create resources and files.
+        $fs = get_file_storage();
+        $resourcegenerator = $this->generator->get_plugin_generator('mod_resource');
+        for ($i = 0; $i < $count; $i++) {
+            // Create resource.
+            $record = array('course' => $this->course->id,
+                    'name' => get_string('bigfile', 'tool_generator', $i));
+            $options = array('section' => $this->get_random_section());
+            $resource = $resourcegenerator->create_instance($record, $options);
+
+            // Write file.
+            $handle = fopen($tempfile, 'w');
+            if (!$handle) {
+                throw new coding_exception('Failed to open temporary file');
+            }
+            for ($j = 0; $j < $blocks; $j++) {
+                $data = self::get_random_binary($blocksize);
+                fwrite($handle, $data);
+                $this->dot($i * $blocks + $j, $count * $blocks);
+            }
+            fclose($handle);
+
+            // Add file.
+            $context = context_module::instance($resource->cmid);
+            $filerecord = array('component' => 'mod_resource', 'filearea' => 'content',
+                    'contextid' => $context->id, 'itemid' => 0, 'filepath' => '/',
+                    'filename' => 'bigfile' . $i . '.dat');
+            $fs->create_file_from_pathname($filerecord, $tempfile);
+        }
+
+        unlink($tempfile);
+        $this->end_log();
+    }
+
+    /**
+     * Creates one forum activity with a bunch of posts.
+     */
+    private function create_forum() {
+        global $DB;
+
+        $discussions = self::$paramforumdiscussions[$this->size];
+        $posts = self::$paramforumposts[$this->size];
+        $totalposts = $discussions * $posts;
+
+        $this->log('createforum', $totalposts, true);
+
+        // Create empty forum.
+        $forumgenerator = $this->generator->get_plugin_generator('mod_forum');
+        $record = array('course' => $this->course->id,
+                'name' => get_string('pluginname', 'forum'));
+        $options = array('section' => 0);
+        $forum = $forumgenerator->create_instance($record, $options);
+
+        // Add discussions and posts.
+        $sofar = 0;
+        for ($i=0; $i < $discussions; $i++) {
+            $record = array('forum' => $forum->id, 'course' => $this->course->id,
+                    'userid' => $this->get_random_user());
+            $discussion = $forumgenerator->create_discussion($record);
+            $parentid = $DB->get_field('forum_posts', 'id', array('discussion' => $discussion->id), MUST_EXIST);
+            $sofar++;
+            for ($j=0; $j < $posts - 1; $j++, $sofar++) {
+                $record = array('discussion' => $discussion->id,
+                        'userid' => $this->get_random_user(), 'parent' => $parentid);
+                $forumgenerator->create_post($record);
+                $this->dot($sofar, $totalposts);
+            }
+        }
+
+        $this->end_log();
+    }
+
+    /**
+     * Gets a random section number.
+     *
+     * @return int A section number from 1 to the number of sections
+     */
+    private function get_random_section() {
+        return rand(1, self::$paramsections[$this->size]);
+    }
+
+    /**
+     * Gets a random user id.
+     *
+     * @return int A user id for a random created user
+     */
+    private function get_random_user() {
+        return $this->userids[rand(1, self::$paramusers[$this->size])];
+    }
+
+    /**
+     * Displays information as part of progress.
+     * @param string $langstring Part of langstring (after progress_)
+     * @param mixed $a Optional lang string parameters
+     * @param bool $leaveopen If true, doesn't close LI tag (ready for dots)
+     */
+    private function log($langstring, $a = null, $leaveopen = false) {
+        if (!$this->progress) {
+            return;
+        }
+        if (CLI_SCRIPT) {
+            echo '* ';
+        } else {
+            echo html_writer::start_tag('li');
+        }
+        echo get_string('progress_' . $langstring, 'tool_generator', $a);
+        if (!$leaveopen) {
+            if (CLI_SCRIPT) {
+                echo "\n";
+            } else {
+                echo html_writer::end_tag('li');
+            }
+        } else {
+            echo ': ';
+            $this->lastdot = time();
+            $this->lastpercentage = $this->lastdot;
+            $this->starttime = microtime(true);
+        }
+    }
+
+    /**
+     * Outputs dots. There is up to one dot per second. Once a minute, it
+     * displays a percentage.
+     * @param int $number Number of completed items
+     * @param int $total Total number of items to complete
+     */
+    private function dot($number, $total) {
+        if (!$this->progress) {
+            return;
+        }
+        $now = time();
+        if ($now == $this->lastdot) {
+            return;
+        }
+        $this->lastdot = $now;
+        if (CLI_SCRIPT) {
+            echo '.';
+        } else {
+            echo ' . ';
+        }
+        if ($now - $this->lastpercentage >= 30) {
+            echo round(100.0 * $number / $total, 1) . '%';
+            $this->lastpercentage = $now;
+        }
+
+        // Update time limit so PHP doesn't time out.
+        if (!CLI_SCRIPT) {
+            set_time_limit(120);
+        }
+    }
+
+    /**
+     * Ends a log string that was started using log function with $leaveopen.
+     */
+    private function end_log() {
+        if (!$this->progress) {
+            return;
+        }
+        echo get_string('done', 'tool_generator', round(microtime(true) - $this->starttime, 1));
+        if (CLI_SCRIPT) {
+            echo "\n";
+        } else {
+            echo html_writer::end_tag('li');
+        }
+    }
+}
diff --git a/admin/tool/generator/classes/make_form.php b/admin/tool/generator/classes/make_form.php
new file mode 100644 (file)
index 0000000..25c8350
--- /dev/null
@@ -0,0 +1,59 @@
+<?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/>.
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir . '/formslib.php');
+
+/**
+ * Form with options for creating large course.
+ *
+ * @package tool_generator
+ * @copyright 2013 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_generator_make_form extends moodleform {
+
+    public function definition() {
+        $mform = $this->_form;
+
+        $mform->addElement('select', 'size', get_string('size', 'tool_generator'),
+                tool_generator_backend::get_size_choices());
+        $mform->setDefault('size', tool_generator_backend::DEFAULT_SIZE);
+
+        $mform->addElement('text', 'shortname', get_string('shortnamecourse'));
+        $mform->addRule('shortname', get_string('missingshortname'), 'required', null, 'client');
+        $mform->setType('shortname', PARAM_TEXT);
+
+        $mform->addElement('submit', 'submit', get_string('createcourse', 'tool_generator'));
+    }
+
+    public function validation($data, $files) {
+        global $DB;
+        $errors = array();
+
+        // Check course doesn't already exist.
+        if (!empty($data['shortname'])) {
+            // Check shortname.
+            $error =  tool_generator_backend::check_shortname_available($data['shortname']);
+            if ($error) {
+                $errors['shortname'] = $error;
+            }
+        }
+
+        return $errors;
+    }
+}
index 4514fd7..353883e 100644 (file)
@@ -28,7 +28,7 @@ define('CLI_SCRIPT', true);
 require(dirname(__FILE__) . '/../../../../config.php');
 require_once(dirname(__FILE__) . '/../locallib.php');
 
-if (!debugging('', DEBUG_DEVELOPER)) {
+if (!$CFG->debugdeveloper) {
     echo("This script is for developers only!!!\n");
     exit(1);
 }
diff --git a/admin/tool/generator/cli/maketestcourse.php b/admin/tool/generator/cli/maketestcourse.php
new file mode 100644 (file)
index 0000000..b646e2f
--- /dev/null
@@ -0,0 +1,94 @@
+<?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 interface for creating a test course.
+ *
+ * @package tool_generator
+ * @copyright 2013 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+define('NO_OUTPUT_BUFFERING', true);
+
+require(dirname(__FILE__) . '/../../../../config.php');
+require_once($CFG->libdir. '/clilib.php');
+
+// CLI options.
+list($options, $unrecognized) = cli_get_params(
+    array(
+        'help' => false,
+        'shortname' => false,
+        'size' => false,
+        'bypasscheck' => false,
+        'quiet' => false
+    ),
+    array(
+        'h' => 'help'
+    )
+);
+
+// Display help.
+if (!empty($options['help']) || empty($options['shortname']) || empty($options['size'])) {
+    echo "
+Utility to create standard test course. (Also available in GUI interface.)
+
+Not for use on live sites; only normally works if debugging is set to DEVELOPER
+level.
+
+Options:
+--shortname    Shortname of course to create (required)
+--size         Size of course to create XS, S, M, L, XL, or XXL (required)
+--bypasscheck  Bypasses the developer-mode check (be careful!)
+--quiet        Do not show any output
+
+-h, --help     Print out this help
+
+Example from Moodle root directory:
+\$ php admin/tool/generator/cli/maketestcourse.php --shortname=SIZE_S --size=S
+";
+    // Exit with error unless we're showing this because they asked for it.
+    exit(empty($options['help']) ? 1 : 0);
+}
+
+// Check debugging is set to developer level.
+if (empty($options['bypasscheck']) && !debugging('', DEBUG_DEVELOPER)) {
+    cli_error(get_string('error_notdebugging', 'tool_generator'));
+}
+
+// Get options.
+$shortname = $options['shortname'];
+$sizename = $options['size'];
+
+// Check size.
+try {
+    $size = tool_generator_backend::size_for_name($sizename);
+} catch (coding_exception $e) {
+    cli_error("Invalid size ($sizename). Use --help for help.");
+}
+
+// Check shortname.
+if ($error = tool_generator_backend::check_shortname_available($shortname)) {
+    cli_error($error);
+}
+
+// Switch to admin user account.
+session_set_user(get_admin());
+
+// Do backend code to generate course.
+$backend = new tool_generator_backend($shortname, $size, empty($options['quiet']));
+$id = $backend->make();
index f77c8e0..7a917e9 100644 (file)
@@ -34,7 +34,7 @@ if (!is_siteadmin()) {
     error('Only for admins');
 }
 
-if (!debugging('', DEBUG_DEVELOPER)) {
+if (!$CFG->debugdeveloper) {
     error('This script is for developers only!!!');
 }
 
index 051d04f..103a4fd 100644 (file)
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Strings for component 'tool_generator', language 'en', branch 'MOODLE_22_STABLE'
+ * Language strings.
  *
- * @package    tool
- * @subpackage generator
- * @copyright  2011 Petr Skoda
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package tool_generator
+ * @copyright 2013 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['bigfile'] = 'Big file {$a}';
+$string['createcourse'] = 'Create course';
+$string['creating'] = 'Creating course';
+$string['done'] = 'done ({$a}s)';
+$string['explanation'] = 'This tool creates standard test courses that include many
+sections, activities, and files.
+
+This is intended to provide a standardised measure for checking the reliability
+and performance of various system components (such as backup and restore).
+
+This test is important because there have been many cases previously where,
+faced with real-life use cases (e.g. a course with 1,000 activities), the system
+does not work.
+
+Courses created using this feature can occupy a large amount of database and
+filesystem space (tens of gigabytes). You will need to delete the courses
+(and wait for various cleanup runs) to release this space again.
+
+**Do not use this feature on a live system**. Use only on a developer server.
+(To avoid accidental use, this feature is disabled unless you have also selected
+DEVELOPER debugging level.)';
+
+$string['error_notdebugging'] = 'Not available on this server because debugging is not set to DEVELOPER';
+$string['firstname'] = 'Test course user';
+$string['fullname'] = 'Test course: {$a->size}';
+$string['maketestcourse'] = 'Make test course';
 $string['pluginname'] = 'Random course generator';
+$string['progress_createcourse'] = 'Creating course {$a}';
+$string['progress_checkaccounts'] = 'Checking user accounts ({$a})';
+$string['progress_createaccounts'] = 'Creating user accounts ({$a->from} - {$a->to})';
+$string['progress_createbigfiles'] = 'Creating big files ({$a})';
+$string['progress_createforum'] = 'Creating forum ({$a} posts)';
+$string['progress_createpages'] = 'Creating pages ({$a})';
+$string['progress_createsmallfiles'] = 'Creating small files ({$a})';
+$string['progress_enrol'] = 'Enrolling users into course ({$a})';
+$string['progress_complete'] = 'Complete ({$a}s)';
+$string['shortsize_0'] = 'XS';
+$string['shortsize_1'] = 'S';
+$string['shortsize_2'] = 'M';
+$string['shortsize_3'] = 'L';
+$string['shortsize_4'] = 'XL';
+$string['shortsize_5'] = 'XXL';
+$string['size'] = 'Size of course';
+$string['size_0'] = 'XS (~10KB; create in ~1 second)';
+$string['size_1'] = 'S (~10MB; create in ~30 seconds)';
+$string['size_2'] = 'M (~100MB; create in ~5 minutes)';
+$string['size_3'] = 'L (~1GB; create in ~1 hour)';
+$string['size_4'] = 'XL (~10GB; create in ~4 hours)';
+$string['size_5'] = 'XXL (~20GB; create in ~8 hours)';
+$string['smallfiles'] = 'Small files';
diff --git a/admin/tool/generator/maketestcourse.php b/admin/tool/generator/maketestcourse.php
new file mode 100644 (file)
index 0000000..f3ff9f7
--- /dev/null
@@ -0,0 +1,70 @@
+<?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/>.
+
+/**
+ * Script creates a standardised large course for testing reliability and
+ * performance.
+ *
+ * @package tool_generator
+ * @copyright 2013 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// Disable buffering so that the progress output displays gradually without
+// needing to call flush().
+define('NO_OUTPUT_BUFFERING', true);
+
+require('../../../config.php');
+
+require_once($CFG->libdir . '/adminlib.php');
+
+// Initialise page and check permissions.
+admin_externalpage_setup('toolgenerator');
+
+// Start page.
+echo $OUTPUT->header();
+echo $OUTPUT->heading(get_string('maketestcourse', 'tool_generator'));
+
+// Information message.
+$context = context_system::instance();
+echo $OUTPUT->box(format_text(get_string('explanation', 'tool_generator'),
+        FORMAT_MARKDOWN, array('context' => $context)));
+
+// Check debugging is set to DEVELOPER.
+if (!debugging('', DEBUG_DEVELOPER)) {
+    echo $OUTPUT->notification(get_string('error_notdebugging', 'tool_generator'));
+    echo $OUTPUT->footer();
+    exit;
+}
+
+// Set up the form.
+$mform = new tool_generator_make_form('maketestcourse.php');
+if ($data = $mform->get_data()) {
+    // Do actual work.
+    echo $OUTPUT->heading(get_string('creating', 'tool_generator'));
+    $backend = new tool_generator_backend($data->shortname, $data->size);
+    $id = $backend->make();
+
+    echo html_writer::div(
+            html_writer::link(new moodle_url('/course/view.php', array('id' => $id)),
+                get_string('continue')));
+} else {
+    // Display form.
+    $mform->display();
+}
+
+// Finish page.
+echo $OUTPUT->footer();
diff --git a/admin/tool/generator/settings.php b/admin/tool/generator/settings.php
new file mode 100644 (file)
index 0000000..39a40ae
--- /dev/null
@@ -0,0 +1,32 @@
+<?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/>.
+
+/**
+ * Admin settings.
+ *
+ * @package tool_generator
+ * @copyright 2013 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+if ($hassiteconfig) {
+    $ADMIN->add('development', new admin_externalpage('toolgenerator',
+            get_string('maketestcourse', 'tool_generator'),
+            $CFG->wwwroot . '/' . $CFG->admin . '/tool/generator/maketestcourse.php'));
+}
+
diff --git a/admin/tool/generator/tests/maketestcourse_test.php b/admin/tool/generator/tests/maketestcourse_test.php
new file mode 100644 (file)
index 0000000..3b2244d
--- /dev/null
@@ -0,0 +1,110 @@
+<?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/>.
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Automated unit testing. This tests the 'make large course' backend,
+ * using the 'XS' option so that it completes quickly.
+ *
+ * @package tool_generator
+ * @copyright 2013 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_generator_maketestcourse_testcase extends advanced_testcase {
+    /**
+     * Creates a small test course and checks all the components have been put in place.
+     */
+    public function test_make_xs_course() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // Create the XS course.
+        $backend = new tool_generator_backend('TOOL_MAKELARGECOURSE_XS', 0, false);
+        $courseid = $backend->make();
+
+        // Get course details.
+        $course = get_course($courseid);
+        $context = context_course::instance($courseid);
+        $modinfo = get_fast_modinfo($course);
+
+        // Check sections (just section 0 plus one other).
+        $this->assertEquals(2, count($modinfo->get_section_info_all()));
+
+        // Check user is enrolled.
+        $users = get_enrolled_users($context);
+        $this->assertEquals(1, count($users));
+        $this->assertEquals('tool_generator_000001', reset($users)->username);
+
+        // Check there's a page on the course.
+        $pages = $modinfo->get_instances_of('page');
+        $this->assertEquals(1, count($pages));
+
+        // Check there are small files.
+        $resources = $modinfo->get_instances_of('resource');
+        $ok = false;
+        foreach ($resources as $resource) {
+            if ($resource->sectionnum == 0) {
+                // The one in section 0 is the 'small files' resource.
+                $ok = true;
+                break;
+            }
+        }
+        $this->assertTrue($ok);
+
+        // Check it contains 2 files (the default txt and a dat file).
+        $fs = get_file_storage();
+        $resourcecontext = context_module::instance($resource->id);
+        $files = $fs->get_area_files($resourcecontext->id, 'mod_resource', 'content', false, 'filename', false);
+        $files = array_values($files);
+        $this->assertEquals(2, count($files));
+        $this->assertEquals('resource1.txt', $files[0]->get_filename());
+        $this->assertEquals('smallfile0.dat', $files[1]->get_filename());
+
+        // Check there's a single 'big' file (it's actually only 8KB).
+        $ok = false;
+        foreach ($resources as $resource) {
+            if ($resource->sectionnum == 1) {
+                $ok = true;
+                break;
+            }
+        }
+        $this->assertTrue($ok);
+
+        // Check it contains 2 files.
+        $resourcecontext = context_module::instance($resource->id);
+        $files = $fs->get_area_files($resourcecontext->id, 'mod_resource', 'content', false, 'filename', false);
+        $files = array_values($files);
+        $this->assertEquals(2, count($files));
+        $this->assertEquals('bigfile0.dat', $files[0]->get_filename());
+        $this->assertEquals('resource2.txt', $files[1]->get_filename());
+
+        // Get forum and count the number of posts on it.
+        $forums = $modinfo->get_instances_of('forum');
+        $forum = reset($forums);
+        $posts = $DB->count_records_sql("
+                SELECT
+                    COUNT(1)
+                FROM
+                    {forum_posts} fp
+                    JOIN {forum_discussions} fd ON fd.id = fp.discussion
+                WHERE
+                    fd.forum = ?", array($forum->instance));
+        $this->assertEquals(2, $posts);
+    }
+}
index 0e01250..66aa662 100644 (file)
 /**
  * Version details.
  *
- * @package    tool
- * @subpackage generator
- * @copyright  2009 Nicolas Connault
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package tool_generator
+ * @copyright 2013 The Open University
+ * @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 = 'tool_generator'; // Full name of the plugin (used for diagnostics)
-
-$plugin->maturity  = MATURITY_ALPHA; // this version's maturity level
+$plugin->version = 2013080700;
+$plugin->requires = 2013080200;
+$plugin->component = 'tool_generator';
index f697f7e..f52bf6b 100644 (file)
@@ -34,7 +34,7 @@ $execute   = optional_param('execute', 0, PARAM_BOOL);
 navigation_node::override_active_url(new moodle_url('/admin/tool/phpunit/index.php'));
 admin_externalpage_setup('toolphpunitwebrunner');
 
-if (!debugging('', DEBUG_DEVELOPER)) {
+if (!$CFG->debugdeveloper) {
     error('Not available on production sites, sorry.');
 }
 
@@ -82,7 +82,7 @@ if ($execute) {
         if ($code != 0) {
             tool_phpunit_problem('Can not initialize database');
         }
-        $CFG->debug = 0; // no pesky redirect warning, we really want to redirect
+        set_debugging(DEBUG_NONE, false); // Hack: no redirect warning, we really want to redirect.
         redirect(new moodle_url($PAGE->url, array('execute'=>1, 'tespath'=>$testpath, 'testclass'=>$testclass, 'sesskey'=>sesskey())), 'Reloading page');
         echo $OUTPUT->footer();
         die();
@@ -103,7 +103,7 @@ if ($execute) {
         if ($code != 0) {
             tool_phpunit_problem('Can not initialize database');
         }
-        $CFG->debug = 0; // no pesky redirect warning, we really want to redirect
+        set_debugging(DEBUG_NONE, false); // Hack: no redirect warning, we really want to redirect.
         redirect(new moodle_url($PAGE->url, array('execute'=>1, 'tespath'=>$testpath, 'testclass'=>$testclass, 'sesskey'=>sesskey())), 'Reloading page');
         die();
 
index ebd0b75..2873f3b 100644 (file)
@@ -37,31 +37,6 @@ require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
  */
 class tool_uploadcourse_helper {
 
-    /**
-     * Remove the restore content from disk and cache.
-     *
-     * @return void
-     */
-    public static function clean_restore_content() {
-        global $CFG;
-
-        // There are some sloppy unclosed file handles in backup/restore code,
-        // let's hope somebody unset all controllers before calling this
-        // and destroy magic will close all remaining open file handles,
-        // otherwise Windows will fail deleting the directory.
-        gc_collect_cycles();
-
-        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.
      *
@@ -216,7 +191,9 @@ class tool_uploadcourse_helper {
      * 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.
+     *
+     * This caches the result for better performance, but $CFG->keeptempdirectoriesonbackup
+     * needs to be enabled, otherwise the cache is ignored.
      *
      * @param string $backupfile path to a backup file.
      * @param string $shortname shortname of a course.
@@ -229,6 +206,10 @@ class tool_uploadcourse_helper {
         $cachekey = null;
         if (!empty($backupfile)) {
             $backupfile = realpath($backupfile);
+            if (empty($backupfile) || !is_readable($backupfile)) {
+                $errors['cannotreadbackupfile'] = new lang_string('cannotreadbackupfile', 'tool_uploadcourse');
+                return false;
+            }
             $cachekey = 'backup_path:' . $backupfile;
         } else if (!empty($shortname) || is_numeric($shortname)) {
             $cachekey = 'backup_sn:' . $shortname;
@@ -238,23 +219,27 @@ class tool_uploadcourse_helper {
             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 $CFG->keeptempdirectoriesonbackup is not set to true, any restore happening would
+        // automatically delete the backup directory... causing the cache to return an unexisting directory.
+        $usecache = !empty($CFG->keeptempdirectoriesonbackup);
+        if ($usecache) {
+            $cache = cache::make('tool_uploadcourse', 'helper');
+        }
+
+        // If we don't use the cache, or if we do and not set, or the directory doesn't exist any more.
+        if (!$usecache || (($backupid = $cache->get($cachekey)) === false || !is_dir("$CFG->tempdir/backup/$backupid"))) {
+
+            // Use null instead of false because it would consider that the cache key has not been set.
+            $backupid = null;
+
             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');
-                    }
+                // 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.
@@ -270,14 +255,15 @@ class tool_uploadcourse_helper {
                         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);
+            if ($usecache) {
+                $cache->set($cachekey, $backupid);
+            }
         }
 
+        if ($backupid === null) {
+            $backupid = false;
+        }
         return $backupid;
     }
 
index 94ab8a7..79b10ed 100644 (file)
@@ -223,8 +223,6 @@ class tool_uploadcourse_processor {
 
         $tracker->finish();
         $tracker->results($total, $created, $updated, $deleted, $errors);
-
-        $this->remove_restore_content();
     }
 
     /**
@@ -349,20 +347,10 @@ class tool_uploadcourse_processor {
         }
 
         $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.
      *
index 7464e4e..32886ef 100644 (file)
@@ -58,11 +58,12 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
         $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);
 
+        // Restore file is not in the array options on purpose, because formslib can't handle it!
         $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('filepicker', 'restorefile', get_string('templatefile', 'tool_uploadcourse'));
+        $mform->addHelpButton('restorefile', 'templatefile', 'tool_uploadcourse');
 
         $mform->addElement('text', 'options[templatecourse]', get_string('coursetemplatename', 'tool_uploadcourse'));
         $mform->setType('options[templatecourse]', PARAM_TEXT);
@@ -188,33 +189,4 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
         $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;
-    }
 }
index 667551e..0f290e2 100644 (file)
@@ -78,6 +78,13 @@ if ($form2data = $mform2->is_cancelled()) {
 
     $options = (array) $form2data->options;
     $defaults = (array) $form2data->defaults;
+
+    // Restorefile deserves its own logic because formslib does not really appreciate
+    // when the name of a filepicker is an array...
+    $options['restorefile'] = '';
+    if (!empty($form2data->restorefile)) {
+        $options['restorefile'] = $mform2->save_temp_file('restorefile');
+    }
     $processor = new tool_uploadcourse_processor($cir, $options, $defaults);
 
     echo $OUTPUT->header();
@@ -91,6 +98,11 @@ if ($form2data = $mform2->is_cancelled()) {
         echo $OUTPUT->continue_button($returnurl);
     }
 
+    // Deleting the file after processing or preview.
+    if (!empty($options['restorefile'])) {
+        @unlink($options['restorefile']);
+    }
+
 } else {
     $processor = new tool_uploadcourse_processor($cir, $form1data->options, array());
     echo $OUTPUT->header();
index ee979b4..3459c4a 100644 (file)
@@ -635,6 +635,23 @@ class tool_uploadcourse_course_testcase extends advanced_testcase {
         }
         $this->assertTrue($found);
 
+        // Restoring twice from the same course should work.
+        $data = array('shortname' => 'B1', 'templatecourse' => $c1->shortname, 'summary' => 'B', 'category' => 1,
+            'fullname' => 'B1');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $course = $DB->get_record('course', array('shortname' => 'B1'));
+        $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);
     }
@@ -668,6 +685,25 @@ class tool_uploadcourse_course_testcase extends advanced_testcase {
         }
         $this->assertTrue($found);
 
+        // Restoring twice from the same file should work.
+        $data = array('shortname' => 'B1', 'backupfile' => __DIR__ . '/fixtures/backup.mbz',
+            'summary' => 'B', 'category' => 1, 'fullname' => 'B1');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $course = $DB->get_record('course', array('shortname' => 'B1'));
+        $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);
     }
index 31e53e0..ae42a1e 100644 (file)
@@ -141,6 +141,9 @@ class tool_uploadcourse_helper_testcase extends advanced_testcase {
         $bc->destroy();
         unset($bc); // File logging is a mess, we can only try to rely on gc to close handles.
 
+        $oldcfg = isset($CFG->keeptempdirectoriesonbackup) ? $CFG->keeptempdirectoriesonbackup : false;
+        $CFG->keeptempdirectoriesonbackup = true;
+
         // Checking restore dir.
         $dir = tool_uploadcourse_helper::get_restore_content_dir($c1backupfile, null);
         $bcinfo = backup_general_helper::get_backup_information($dir);
@@ -179,18 +182,26 @@ class tool_uploadcourse_helper_testcase extends advanced_testcase {
         $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));
-
+        // Trying again without caching. $CFG->keeptempdirectoriesonbackup is required for caching.
         $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));
+        // Checking restore dir.
+        $dir = tool_uploadcourse_helper::get_restore_content_dir($c1backupfile, null);
+        $dir2 = tool_uploadcourse_helper::get_restore_content_dir($c1backupfile, null);
+        $this->assertNotEquals($dir, $dir2);
+
+        // Checking with a shortname.
+        $dir = tool_uploadcourse_helper::get_restore_content_dir(null, $c1->shortname);
+        $dir2 = tool_uploadcourse_helper::get_restore_content_dir(null, $c1->shortname);
+        $this->assertNotEquals($dir, $dir2);
+
+        // 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);
+        $dir2 = tool_uploadcourse_helper::get_restore_content_dir(null, 'DoesNotExist', $errors);
+        $this->assertEquals($dir, $dir2);
 
         $CFG->keeptempdirectoriesonbackup = $oldcfg;
 
index 33cf33a..a02c32f 100644 (file)
@@ -48,7 +48,7 @@ require(dirname(dirname(dirname(dirname(__FILE__)))).'/config.php');
 require_once($CFG->dirroot.'/course/lib.php');
 
 // Ensure errors are well explained
-$CFG->debug = DEBUG_NORMAL;
+set_debugging(DEBUG_DEVELOPER, true);
 
 if (!is_enabled_auth('cas')) {
     error_log('[AUTH CAS] '.get_string('pluginnotenabled', 'auth_ldap'));
index a1a9645..866a838 100644 (file)
@@ -1646,8 +1646,7 @@ class auth_plugin_ldap extends auth_plugin_base {
             // Now start the whole NTLM machinery.
             if($this->config->ntlmsso_ie_fastpath == AUTH_NTLM_FASTPATH_YESATTEMPT ||
                 $this->config->ntlmsso_ie_fastpath == AUTH_NTLM_FASTPATH_YESFORM) {
-
-                if(check_browser_version('MSIE')) {
+                if (core_useragent::check_ie_version()) {
                     $sesskey = sesskey();
                     redirect($CFG->wwwroot.'/auth/ldap/ntlmsso_magic.php?sesskey='.$sesskey);
                 } else if ($this->config->ntlmsso_ie_fastpath == AUTH_NTLM_FASTPATH_YESFORM) {
index ee15538..6898293 100644 (file)
@@ -52,7 +52,7 @@ require(dirname(dirname(dirname(dirname(__FILE__)))).'/config.php'); // global m
 require_once($CFG->dirroot.'/course/lib.php');
 
 // Ensure errors are well explained
-$CFG->debug = DEBUG_NORMAL;
+set_debugging(DEBUG_DEVELOPER, true);
 
 if (!is_enabled_auth('ldap')) {
     error_log('[AUTH LDAP] '.get_string('pluginnotenabled', 'auth_ldap'));
index 0d349c5..2ac49a3 100644 (file)
@@ -28,7 +28,7 @@ $file = $CFG->dirroot.'/pix/spacer.gif';
 
 if ($authplugin->ntlmsso_magic($sesskey) && file_exists($file)) {
     if (!empty($authplugin->config->ntlmsso_ie_fastpath)) {
-        if (check_browser_version('MSIE')) {
+        if (core_useragent::check_ie_version()) {
             // $PAGE->https_required() up above takes care of what $CFG->httpswwwroot should be.
             redirect($CFG->httpswwwroot.'/auth/ldap/ntlmsso_finish.php');
         }
index 6dd5038..fbd919a 100644 (file)
@@ -113,31 +113,46 @@ if ($backup->get_stage() == backup_ui::STAGE_FINAL) {
     // Mark the UI finished.
     $rc->finish_ui();
     // Execute prechecks
+    $warnings = false;
     if (!$rc->execute_precheck()) {
         $precheckresults = $rc->get_precheck_results();
-        if (is_array($precheckresults) && !empty($precheckresults['errors'])) {
-            fulldelete($tempdestination);
-
-            echo $OUTPUT->header();
-            echo $renderer->precheck_notices($precheckresults);
-            echo $OUTPUT->continue_button(new moodle_url('/course/view.php', array('id'=>$course->id)));
-            echo $OUTPUT->footer();
-            die();
+        if (is_array($precheckresults)) {
+            if (!empty($precheckresults['errors'])) { // If errors are found, terminate the import.
+                fulldelete($tempdestination);
+
+                echo $OUTPUT->header();
+                echo $renderer->precheck_notices($precheckresults);
+                echo $OUTPUT->continue_button(new moodle_url('/course/view.php', array('id'=>$course->id)));
+                echo $OUTPUT->footer();
+                die();
+            }
+            if (!empty($precheckresults['warnings'])) { // If warnings are found, go ahead but display warnings later.
+                $warnings = $precheckresults['warnings'];
+            }
         }
-    } else {
-        if ($restoretarget == backup::TARGET_CURRENT_DELETING || $restoretarget == backup::TARGET_EXISTING_DELETING) {
-            restore_dbops::delete_course_content($course->id);
-        }
-        // Execute the restore
-        $rc->execute_plan();
     }
+    if ($restoretarget == backup::TARGET_CURRENT_DELETING || $restoretarget == backup::TARGET_EXISTING_DELETING) {
+        restore_dbops::delete_course_content($course->id);
+    }
+    // Execute the restore.
+    $rc->execute_plan();
 
     // Delete the temp directory now
     fulldelete($tempdestination);
 
     // Display a notification and a continue button
     echo $OUTPUT->header();
-    echo $OUTPUT->notification(get_string('importsuccess', 'backup'),'notifysuccess');
+    if ($warnings) {
+        echo $OUTPUT->box_start();
+        echo $OUTPUT->notification(get_string('warning'), 'notifywarning');
+        echo html_writer::start_tag('ul', array('class'=>'list'));
+        foreach ($warnings as $warning) {
+            echo html_writer::tag('li', $warning);
+        }
+        echo html_writer::end_tag('ul');
+        echo $OUTPUT->box_end();
+    }
+    echo $OUTPUT->notification(get_string('importsuccess', 'backup'), 'notifysuccess');
     echo $OUTPUT->continue_button(new moodle_url('/course/view.php', array('id'=>$course->id)));
     echo $OUTPUT->footer();
 
index 99365ba..60f599a 100644 (file)
@@ -35,6 +35,18 @@ defined('MOODLE_INTERNAL') || die();
  */
 abstract class restore_qtype_plugin extends restore_plugin {
 
+    /*
+     * A simple answer to id cache for a single questions answers.
+     * @var array
+     */
+    private $questionanswercache = array();
+
+    /*
+     * The id of the current question in the questionanswercache.
+     * @var int
+     */
+    private $questionanswercacheid = null;
+
     /**
      * Add to $paths the restore_path_elements needed
      * to handle question_answers for a given question
@@ -147,38 +159,32 @@ abstract class restore_qtype_plugin extends restore_plugin {
 
         // The question existed, we need to map the existing question_answers
         } else {
-            // Look in question_answers by answertext matching
-            $sql = 'SELECT id
-                      FROM {question_answers}
-                     WHERE question = ?
-                       AND ' . $DB->sql_compare_text('answer', 255) . ' = ' . $DB->sql_compare_text('?', 255);
-            $params = array($newquestionid, $data->answertext);
-            $newitemid = $DB->get_field_sql($sql, $params);
-
-            // Not able to find the answer, let's try cleaning the answertext
-            // of all the question answers in DB as slower fallback. MDL-30018.
-            if (!$newitemid) {
+            // Have we cached the current question?
+            if ($this->questionanswercacheid !== $newquestionid) {
+                // The question changed, purge and start again!
+                $this->questionanswercache = array();
                 $params = array('question' => $newquestionid);
                 $answers = $DB->get_records('question_answers', $params, '', 'id, answer');
+                $this->questionanswercacheid = $newquestionid;
+                // Cache all cleaned answers for a simple text match.
                 foreach ($answers as $answer) {
-                    // Clean in the same way than {@link xml_writer::xml_safe_utf8()}.
+                    // MDL-30018: Clean in the same way as {@link xml_writer::xml_safe_utf8()}.
                     $clean = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is','', $answer->answer); // Clean CTRL chars.
                     $clean = preg_replace("/\r\n|\r/", "\n", $clean); // Normalize line ending.
-                    if ($clean === $data->answertext) {
-                        $newitemid = $data->id;
-                    }
+                    $this->questionanswercache[$clean] = $answer->id;
                 }
             }
 
-            // If we haven't found the newitemid, something has gone really wrong, question in DB
-            // is missing answers, exception
-            if (!$newitemid) {
+            if (!isset($this->questionanswercache[$data->answertext])) {
+                // If we haven't found the matching answer, something has gone really wrong, the question in the DB
+                // is missing answers, throw an exception.
                 $info = new stdClass();
                 $info->filequestionid = $oldquestionid;
                 $info->dbquestionid   = $newquestionid;
                 $info->answer         = $data->answertext;
                 throw new restore_step_exception('error_question_answers_missing_in_db', $info);
             }
+            $newitemid = $this->questionanswercache[$data->answertext];
         }
         // Create mapping (we'll use this intensively when restoring question_states. And also answerfeedback files)
         $this->set_mapping('question_answer', $oldid, $newitemid);
index e2143cd..6f8ba31 100644 (file)
@@ -40,7 +40,7 @@ abstract class backup_factory {
         global $CFG;
 
         $dfltloglevel = backup::LOG_WARNING; // Default logging level
-        if (debugging('', DEBUG_DEVELOPER)) { // Debug developer raises default logging level
+        if ($CFG->debugdeveloper) { // Debug developer raises default logging level
             $dfltloglevel = backup::LOG_DEBUG;
         }
 
index 54dc4e9..2a3fa1a 100644 (file)
@@ -94,7 +94,6 @@ class backup_factories_testcase extends advanced_testcase {
 
         // Instantiate with debugging enabled and $CFG->backup_error_log_logger_level not set
         $CFG->debugdisplay = true;
-        $CFG->debug = DEBUG_DEVELOPER;
         unset($CFG->backup_error_log_logger_level);
         $logger1 = backup_factory::get_logger_chain(backup::INTERACTIVE_YES, backup::EXECUTION_INMEDIATE, 'test');
         $this->assertTrue($logger1 instanceof error_log_logger);  // 1st logger is error_log_logger
index 9943e0b..c9dd8fb 100644 (file)
@@ -157,15 +157,18 @@ class restore_plan extends base_plan implements loggable {
         parent::execute();
         $this->controller->set_status(backup::STATUS_FINISHED_OK);
 
-        events_trigger('course_restored', (object) array(
-            'courseid'  => $this->get_courseid(), // The new course
-            'userid'    => $this->get_userid(), // User doing the restore
-            'type'      => $this->controller->get_type(), // backup::TYPE_* constant
-            'target'    => $this->controller->get_target(), // backup::TARGET_* constant
-            'mode'      => $this->controller->get_mode(), // backup::MODE_* constant
-            'operation' => $this->controller->get_operation(), // backup::OPERATION_* constant
-            'samesite'  => $this->controller->is_samesite(),
+        // Trigger a course restored event.
+        $event = \core\event\course_restored::create(array(
+            'objectid' => $this->get_courseid(),
+            'userid' => $this->get_userid(),
+            'context' => context_course::instance($this->get_courseid()),
+            'other' => array('type' => $this->controller->get_type(),
+                             'target' => $this->controller->get_target(),
+                             'mode' => $this->controller->get_mode(),
+                             'operation' => $this->controller->get_operation(),
+                             'samesite' => $this->controller->is_samesite())
         ));
+        $event->trigger();
     }
 
     /**
index 30987e0..8bccf53 100644 (file)
@@ -26,7 +26,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-require_once(dirname(dirname(dirname(__FILE__))) . '/config.php');
 require_once($CFG->libdir . '/badgeslib.php');
 require_once($CFG->dirroot . '/user/selector/lib.php');
 
index f6abcb6..088b443 100644 (file)
@@ -26,7 +26,6 @@
 
 require_once($CFG->libdir . '/badgeslib.php');
 require_once($CFG->libdir . '/tablelib.php');
-require_once($CFG->dirroot . '/user/filters/lib.php');
 
 /**
  * Standard HTML output renderer for badges
index 9d9b289..461baf1 100644 (file)
@@ -416,6 +416,9 @@ class block_base {
             'class' => 'block_' . $this->name(). '  block',
             'role' => $this->get_aria_role()
         );
+        if ($this->hide_header()) {
+            $attributes['class'] .= ' no-header';
+        }
         if ($this->instance_can_be_docked() && get_user_preferences('docked_block_instance_'.$this->instance->id, 0)) {
             $attributes['class'] .= ' dock_on_load';
         }
index 1035614..6b1a793 100644 (file)
@@ -110,12 +110,12 @@ class block_tags extends block_base {
             $content = '';
             $moretags = new moodle_url('/tag/coursetags_more.php', array('show'=>$tagtype));
             if ($tagtype == 'all') {
-                $tags = coursetag_get_tags(0, 0, $this->config->tagtype, $this->config->numberoftags, 'name');
+                $tags = coursetag_get_tags(0, 0, $this->config->tagtype, $this->config->numberoftags);
             } else if ($tagtype == 'course') {
-                $tags = coursetag_get_tags($this->page->course->id, 0, $this->config->tagtype, $this->config->numberoftags, 'name');
+                $tags = coursetag_get_tags($this->page->course->id, 0, $this->config->tagtype, $this->config->numberoftags);
                 $moretags->param('courseid', $this->page->course->id);
             } else if ($tagtype == 'my') {
-                $tags = coursetag_get_tags(0, $USER->id, $this->config->tagtype, $this->config->numberoftags, 'name');
+                $tags = coursetag_get_tags(0, $USER->id, $this->config->tagtype, $this->config->numberoftags);
             }
             $tagcloud = tag_print_cloud($tags, 150, true);
             if (!$tagcloud) {
index adc1a9b..4830d71 100644 (file)
@@ -336,6 +336,7 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
         $filename = $key.'.cache';
         $file = $this->file_path_for_key($key);
         $ttl = $this->definition->get_ttl();
+        $maxtime = 0;
         if ($ttl) {
             $maxtime = cache::now() - $ttl;
         }
index 6a1630d..c30ae0c 100644 (file)
@@ -189,11 +189,8 @@ class core_cache_administration_helper_testcase extends advanced_testcase {
      * Test the hash_key functionality.
      */
     public function test_hash_key() {
-        global $CFG;
-
-        $currentdebugging = $CFG->debug;
-
-        $CFG->debug = E_ALL;
+        $this->resetAfterTest();
+        set_debugging(DEBUG_ALL);
 
         // First with simplekeys
         $instance = cache_config_phpunittest::instance(true);
@@ -230,7 +227,5 @@ class core_cache_administration_helper_testcase extends advanced_testcase {
 
         $result = cache_helper::hash_key('test/test', $definition);
         $this->assertEquals(sha1($definition->generate_single_key_prefix().'-test/test'), $result);
-
-        $CFG->debug = $currentdebugging;
     }
 }
index 86c265f..7fed0c0 100644 (file)
@@ -1410,14 +1410,8 @@ function calendar_get_module_cached(&$coursecache, $modulename, $instance) {
  * @return stdClass $coursecache[$courseid] return the specific course cache
  */
 function calendar_get_course_cached(&$coursecache, $courseid) {
-    global $COURSE, $DB;
-
     if (!isset($coursecache[$courseid])) {
-        if ($courseid == $COURSE->id) {
-            $coursecache[$courseid] = $COURSE;
-        } else {
-            $coursecache[$courseid] = $DB->get_record('course', array('id'=>$courseid));
-        }
+        $coursecache[$courseid] = get_course($courseid);
     }
     return $coursecache[$courseid];
 }
diff --git a/calendar/tests/lib_test.php b/calendar/tests/lib_test.php
new file mode 100644 (file)
index 0000000..c405b96
--- /dev/null
@@ -0,0 +1,71 @@
+<?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/>.
+
+/**
+ * Calendar lib unit tests
+ *
+ * @package    core_calendar
+ * @copyright  2013 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+global $CFG;
+require_once($CFG->dirroot . '/calendar/lib.php');
+
+/**
+ * Unit tests for calendar lib
+ *
+ * @package    core_calendar
+ * @copyright  2013 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_calendar_lib_testcase extends advanced_testcase {
+
+    public function test_calendar_get_course_cached() {
+        $this->resetAfterTest(true);
+
+        // Setup some test courses.
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+        $course3 = $this->getDataGenerator()->create_course();
+
+        // Load courses into cache.
+        $coursecache = null;
+        calendar_get_course_cached($coursecache, $course1->id);
+        calendar_get_course_cached($coursecache, $course2->id);
+        calendar_get_course_cached($coursecache, $course3->id);
+
+        // Verify the cache.
+        $this->assertArrayHasKey($course1->id, $coursecache);
+        $cachedcourse1 = $coursecache[$course1->id];
+        $this->assertEquals($course1->id, $cachedcourse1->id);
+        $this->assertEquals($course1->shortname, $cachedcourse1->shortname);
+        $this->assertEquals($course1->fullname, $cachedcourse1->fullname);
+
+        $this->assertArrayHasKey($course2->id, $coursecache);
+        $cachedcourse2 = $coursecache[$course2->id];
+        $this->assertEquals($course2->id, $cachedcourse2->id);
+        $this->assertEquals($course2->shortname, $cachedcourse2->shortname);
+        $this->assertEquals($course2->fullname, $cachedcourse2->fullname);
+
+        $this->assertArrayHasKey($course3->id, $coursecache);
+        $cachedcourse3 = $coursecache[$course3->id];
+        $this->assertEquals($course3->id, $cachedcourse3->id);
+        $this->assertEquals($course3->shortname, $cachedcourse3->shortname);
+        $this->assertEquals($course3->fullname, $cachedcourse3->fullname);
+    }
+}
index 61d56c9..51ac01c 100644 (file)
@@ -57,7 +57,12 @@ function cohort_add_cohort($cohort) {
 
     $cohort->id = $DB->insert_record('cohort', $cohort);
 
-    events_trigger('cohort_added', $cohort);
+    $event = \core\event\cohort_created::create(array(
+        'context' => context::instance_by_id($cohort->contextid),
+        'objectid' => $cohort->id,
+    ));
+    $event->add_record_snapshot('cohort', $cohort);
+    $event->trigger();
 
     return $cohort->id;
 }
@@ -76,7 +81,12 @@ function cohort_update_cohort($cohort) {
     $cohort->timemodified = time();
     $DB->update_record('cohort', $cohort);
 
-    events_trigger('cohort_updated', $cohort);
+    $event = \core\event\cohort_updated::create(array(
+        'context' => context::instance_by_id($cohort->contextid),
+        'objectid' => $cohort->id,
+    ));
+    $event->add_record_snapshot('cohort', $cohort);
+    $event->trigger();
 }
 
 /**
@@ -94,7 +104,12 @@ function cohort_delete_cohort($cohort) {
     $DB->delete_records('cohort_members', array('cohortid'=>$cohort->id));
     $DB->delete_records('cohort', array('id'=>$cohort->id));
 
-    events_trigger('cohort_deleted', $cohort);
+    $event = \core\event\cohort_deleted::create(array(
+        'context' => context::instance_by_id($cohort->contextid),
+        'objectid' => $cohort->id,
+    ));
+    $event->add_record_snapshot('cohort', $cohort);
+    $event->trigger();
 }
 
 /**
@@ -141,7 +156,15 @@ function cohort_add_member($cohortid, $userid) {
     $record->timeadded = time();
     $DB->insert_record('cohort_members', $record);
 
-    events_trigger('cohort_member_added', (object)array('cohortid'=>$cohortid, 'userid'=>$userid));
+    $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST);
+
+    $event = \core\event\cohort_member_added::create(array(
+        'context' => context::instance_by_id($cohort->contextid),
+        'objectid' => $cohortid,
+        'relateduserid' => $userid,
+    ));
+    $event->add_record_snapshot('cohort', $cohort);
+    $event->trigger();
 }
 
 /**
@@ -154,7 +177,15 @@ function cohort_remove_member($cohortid, $userid) {
     global $DB;
     $DB->delete_records('cohort_members', array('cohortid'=>$cohortid, 'userid'=>$userid));
 
-    events_trigger('cohort_member_removed', (object)array('cohortid'=>$cohortid, 'userid'=>$userid));
+    $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST);
+
+    $event = \core\event\cohort_member_removed::create(array(
+        'context' => context::instance_by_id($cohort->contextid),
+        'objectid' => $cohortid,
+        'relateduserid' => $userid,
+    ));
+    $event->add_record_snapshot('cohort', $cohort);
+    $event->trigger();
 }
 
 /**
index e6446e7..64f40c7 100644 (file)
@@ -62,20 +62,50 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertNotEmpty($newcohort->timecreated);
         $this->assertSame($newcohort->component, '');
         $this->assertSame($newcohort->timecreated, $newcohort->timemodified);
+    }
+
+    public function test_cohort_add_cohort_missing_name() {
+        $cohort = new stdClass();
+        $cohort->contextid = context_system::instance()->id;
+        $cohort->name = null;
+        $cohort->idnumber = 'testid';
+        $cohort->description = 'test cohort desc';
+        $cohort->descriptionformat = FORMAT_HTML;
+
+        $this->setExpectedException('coding_exception', 'Missing cohort name in cohort_add_cohort().');
+        cohort_add_cohort($cohort);
+    }
+
+    public function test_cohort_add_cohort_event() {
+        $this->resetAfterTest();
+
+        // Setup cohort data structure.
+        $cohort = new stdClass();
+        $cohort->contextid = context_system::instance()->id;
+        $cohort->name = 'test cohort';
+        $cohort->idnumber = 'testid';
+        $cohort->description = 'test cohort desc';
+        $cohort->descriptionformat = FORMAT_HTML;
 
-        try {
-            $cohort = new stdClass();
-            $cohort->contextid = context_system::instance()->id;
-            $cohort->name = null;
-            $cohort->idnumber = 'testid';
-            $cohort->description = 'test cohort desc';
-            $cohort->descriptionformat = FORMAT_HTML;
-            cohort_add_cohort($cohort);
-
-            $this->fail('Exception expected when trying to add cohort without name');
-        } catch (Exception $e) {
-            $this->assertInstanceOf('coding_exception', $e);
-        }
+        // Catch Events.
+        $sink = $this->redirectEvents();
+
+        // Perform the add operation.
+        $id = cohort_add_cohort($cohort);
+
+        // Capture the event.
+        $events = $sink->get_events();
+        $sink->close();
+
+        // Validate the event.
+        $this->assertCount(1, $events);
+        $event = $events[0];
+        $this->assertInstanceOf('\core\event\cohort_created', $event);
+        $this->assertEquals('cohort', $event->objecttable);
+        $this->assertEquals($id, $event->objectid);
+        $this->assertEquals($cohort->contextid, $event->contextid);
+        $this->assertEquals($cohort, $event->get_record_snapshot('cohort', $id));
+        $this->assertEventLegacyData($cohort, $event);
     }
 
     public function test_cohort_update_cohort() {
@@ -110,6 +140,44 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertLessThanOrEqual(time(), $newcohort->timemodified);
     }
 
+    public function test_cohort_update_cohort_event() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Setup the cohort data structure.
+        $cohort = new stdClass();
+        $cohort->contextid = context_system::instance()->id;
+        $cohort->name = 'test cohort';
+        $cohort->idnumber = 'testid';
+        $cohort->description = 'test cohort desc';
+        $cohort->descriptionformat = FORMAT_HTML;
+        $id = cohort_add_cohort($cohort);
+        $this->assertNotEmpty($id);
+
+        $cohort->name = 'test cohort 2';
+
+        // Catch Events.
+        $sink = $this->redirectEvents();
+
+        // Peform the update.
+        cohort_update_cohort($cohort);
+
+        $events = $sink->get_events();
+        $sink->close();
+
+        // Validate the event.
+        $this->assertCount(1, $events);
+        $event = $events[0];
+        $updatedcohort = $DB->get_record('cohort', array('id'=>$id));
+        $this->assertInstanceOf('\core\event\cohort_updated', $event);
+        $this->assertEquals('cohort', $event->objecttable);
+        $this->assertEquals($updatedcohort->id, $event->objectid);
+        $this->assertEquals($updatedcohort->contextid, $event->contextid);
+        $this->assertEquals($cohort, $event->get_record_snapshot('cohort', $id));
+        $this->assertEventLegacyData($cohort, $event);
+    }
+
     public function test_cohort_delete_cohort() {
         global $DB;
 
@@ -122,6 +190,31 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertFalse($DB->record_exists('cohort', array('id'=>$cohort->id)));
     }
 
+    public function test_cohort_delete_cohort_event() {
+
+        $this->resetAfterTest();
+
+        $cohort = $this->getDataGenerator()->create_cohort();
+
+        // Capture the events.
+        $sink = $this->redirectEvents();
+
+        // Perform the delete.
+        cohort_delete_cohort($cohort);
+
+        $events = $sink->get_events();
+        $sink->close();
+
+        // Validate the event structure.
+        $this->assertCount(1, $events);
+        $event = $events[0];
+        $this->assertInstanceOf('\core\event\cohort_deleted', $event);
+        $this->assertEquals('cohort', $event->objecttable);
+        $this->assertEquals($cohort->id, $event->objectid);
+        $this->assertEquals($cohort, $event->get_record_snapshot('cohort', $cohort->id));
+        $this->assertEventLegacyData($cohort, $event);
+    }
+
     public function test_cohort_delete_category() {
         global $DB;
 
@@ -151,6 +244,34 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertTrue($DB->record_exists('cohort_members', array('cohortid'=>$cohort->id, 'userid'=>$user->id)));
     }
 
+    public function test_cohort_add_member_event() {
+        global $USER;
+        $this->resetAfterTest();
+
+        // Setup the data.
+        $cohort = $this->getDataGenerator()->create_cohort();
+        $user = $this->getDataGenerator()->create_user();
+
+        // Capture the events.
+        $sink = $this->redirectEvents();
+
+        // Peform the add member operation.
+        cohort_add_member($cohort->id, $user->id);
+
+        $events = $sink->get_events();
+        $sink->close();
+
+        // Validate the event.
+        $this->assertCount(1, $events);
+        $event = $events[0];
+        $this->assertInstanceOf('\core\event\cohort_member_added', $event);
+        $this->assertEquals('cohort', $event->objecttable);
+        $this->assertEquals($cohort->id, $event->objectid);
+        $this->assertEquals($user->id, $event->relateduserid);
+        $this->assertEquals($USER->id, $event->userid);
+        $this->assertEventLegacyData((object) array('cohortid' => $cohort->id, 'userid' => $user->id), $event);
+    }
+
     public function test_cohort_remove_member() {
         global $DB;
 
@@ -166,6 +287,34 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertFalse($DB->record_exists('cohort_members', array('cohortid'=>$cohort->id, 'userid'=>$user->id)));
     }
 
+    public function test_cohort_remove_member_event() {
+        global $USER;
+        $this->resetAfterTest();
+
+        // Setup the data.
+        $cohort = $this->getDataGenerator()->create_cohort();
+        $user = $this->getDataGenerator()->create_user();
+        cohort_add_member($cohort->id, $user->id);
+
+        // Capture the events.
+        $sink = $this->redirectEvents();
+
+        // Peform the remove operation.
+        cohort_remove_member($cohort->id, $user->id);
+        $events = $sink->get_events();
+        $sink->close();
+
+        // Validate the event.
+        $this->assertCount(1, $events);
+        $event = $events[0];
+        $this->assertInstanceOf('\core\event\cohort_member_removed', $event);
+        $this->assertEquals('cohort', $event->objecttable);
+        $this->assertEquals($cohort->id, $event->objectid);
+        $this->assertEquals($user->id, $event->relateduserid);
+        $this->assertEquals($USER->id, $event->userid);
+        $this->assertEventLegacyData((object) array('cohortid' => $cohort->id, 'userid' => $user->id), $event);
+    }
+
     public function test_cohort_is_member() {
         global $DB;
 
index 8c3b733..0b2a9b1 100644 (file)
@@ -464,10 +464,10 @@ $CFG->admin = 'admin';
 // $CFG->debugusers = '2';
 //
 // Prevent theme caching
-// $CFG->themerev = -1; // NOT FOR PRODUCTION SERVERS!
+// $CFG->themedesignermode = true; // NOT FOR PRODUCTION SERVERS!
 //
 // Prevent JS caching
-// $CFG->jsrev = -1; // NOT FOR PRODUCTION SERVERS!
+// $CFG->cachejs = false; // NOT FOR PRODUCTION SERVERS!
 //
 // Prevent core_string_manager application caching
 // $CFG->langstringcache = false; // NOT FOR PRODUCTION SERVERS!
index 8c16a87..12e4859 100644 (file)
@@ -134,8 +134,14 @@ if ($form->is_cancelled()){
     $aggregation->setMethod($data->role_aggregation);
     $aggregation->save();
 
-    // Log changes.
-    add_to_log($course->id, 'course', 'completion updated', 'completion.php?id='.$course->id);
+    // Trigger an event for course module completion changed.
+    $event = \core\event\course_completion_updated::create(
+            array(
+                'courseid' => $course->id,
+                'context' => context_course::instance($course->id)
+                )
+            );
+    $event->trigger();
 
     // Redirect to the course main page.
     $url = new moodle_url('/course/view.php', array('id' => $course->id));
index 71bafca..26c3de2 100644 (file)
         print_error('confirmsesskeybad', 'error');
     }
 
-    // OK checks done, delete the course now.
-
-    add_to_log(SITEID, "course", "delete", "view.php?id=$course->id", "$course->fullname (ID $course->id)");
-
     $strdeletingcourse = get_string("deletingcourse", "", $courseshortname);
 
     $PAGE->navbar->add($strdeletingcourse);
index c75e3dc..54db939 100644 (file)
@@ -25,6 +25,7 @@
 
 require_once("../config.php");
 require_once("lib.php");
+require_once($CFG->libdir . '/formslib.php');
 require_once($CFG->libdir . '/conditionlib.php');
 
 $id = required_param('id', PARAM_INT);    // course_sections.id
@@ -40,7 +41,7 @@ require_login($course);
 $context = context_course::instance($course->id);
 require_capability('moodle/course:update', $context);
 
-// get section_info object with all availability options
+// Get section_info object with all availability options.
 $sectioninfo = get_fast_modinfo($course)->get_section_info($sectionnum);
 
 $editoroptions = array('context'=>$context ,'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes'=>$CFG->maxbytes, 'trusttext'=>false, 'noclean'=>true);
@@ -51,31 +52,44 @@ $mform = course_get_format($course->id)->editsection_form($PAGE->url,
 $mform->set_data(convert_to_array($sectioninfo));
 
 if ($mform->is_cancelled()){
-    // form cancelled, return to course
+    // Form cancelled, return to course.
     redirect(course_get_url($course, $section, array('sr' => $sectionreturn)));
 } else if ($data = $mform->get_data()) {
-    // data submitted and validated, update and return to course
+    // Data submitted and validated, update and return to course.
     $DB->update_record('course_sections', $data);
     rebuild_course_cache($course->id, true);
     if (isset($data->section)) {
-        // usually edit form does not change relative section number but just in case
+        // Usually edit form does not change relative section number but just in case.
         $sectionnum = $data->section;
     }
     if (!empty($CFG->enableavailability)) {
-        // Update grade and completion conditions
+        // Update grade and completion conditions.
         $sectioninfo = get_fast_modinfo($course)->get_section_info($sectionnum);
         condition_info_section::update_section_from_form($sectioninfo, $data);
         rebuild_course_cache($course->id, true);
     }
     course_get_format($course->id)->update_section_format_options($data);
 
-    add_to_log($course->id, "course", "editsection", "editsection.php?id=$id", "$sectionnum");
+    // Set section info, as this might not be present in form_data.
+    if (!isset($data->section))  {
+        $data->section = $sectionnum;
+    }
+    // Trigger an event for course section update.
+    $event = \core\event\course_section_updated::create(
+            array(
+                'objectid' => $data->id,
+                'courseid' => $course->id,
+                'context' => $context,
+                'other' => array('sectionnum' => $data->section)
+            )
+        );
+    $event->trigger();
+
     $PAGE->navigation->clear_cache();
     redirect(course_get_url($course, $section, array('sr' => $sectionreturn)));
 }
 
-// the edit form is displayed for the first time or there was a validation
-// error on the previous step. Display the edit form:
+// The edit form is displayed for the first time or if there was validation error on the previous step.
 $sectionname  = get_section_name($course, $sectionnum);
 $stredit      = get_string('edita', '', " $sectionname");
 $strsummaryof = get_string('summaryof', '', " $sectionname");
index fe69315..bfc5641 100644 (file)
@@ -172,9 +172,11 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         // When on a section page, we only display the general section title, if title is not the default one
         $hasnamesecpg = ($onsectionpage && ($section->section == 0 && !is_null($section->name)));
 
+        $classes = ' accesshide';
         if ($hasnamenotsecpg || $hasnamesecpg) {
-            $o.= $this->output->heading($this->section_title($section, $course), 3, 'sectionname');
+            $classes = '';
         }
+        $o.= $this->output->heading($this->section_title($section, $course), 3, 'sectionname' . $classes);
 
         $o.= html_writer::start_tag('div', array('class' => 'summary'));
         $o.= $this->format_summary_text($section);
index 31f539c..e1e1240 100644 (file)
@@ -28,7 +28,6 @@ defined('MOODLE_INTERNAL') || die;
 
 require_once($CFG->libdir.'/completionlib.php');
 require_once($CFG->libdir.'/filelib.php');
-require_once($CFG->dirroot.'/course/dnduploadlib.php');
 require_once($CFG->dirroot.'/course/format/lib.php');
 
 define('COURSE_MAX_LOGS_PER_PAGE', 1000);       // records
@@ -959,7 +958,9 @@ function get_array_of_activities($courseid) {
                                    $mod[$seq]->extraclasses = $info->extraclasses;
                                }
                                if (!empty($info->iconurl)) {
-                                   $mod[$seq]->iconurl = $info->iconurl;
+                                   // Convert URL to string as it's easier to store. Also serialized object contains \0 byte and can not be written to Postgres DB.
+                                   $url = new moodle_url($info->iconurl);
+                                   $mod[$seq]->iconurl = $url->out(false);
                                }
                                if (!empty($info->onclick)) {
                                    $mod[$seq]->onclick = $info->onclick;
@@ -1951,7 +1952,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
                 array('class' => 'editing_'. $actionname, 'data-action' => $actionname, 'data-nextgroupmode' => $nextgroupmode)
             );
         } else {
-            $actions[$actionname] = new pix_icon($groupimage, $forcedgrouptitle, 'moodle', array('title' => '', 'class' => 'iconsmall'));
+            $actions[$actionname] = new pix_icon($groupimage, $forcedgrouptitle, 'moodle', array('class' => 'iconsmall'));
         }
     }
 
@@ -2026,14 +2027,14 @@ function course_allowed_module($course, $modname) {
  * @return bool success
  */
 function move_courses($courseids, $categoryid) {
-    global $CFG, $DB, $OUTPUT;
+    global $DB;
 
     if (empty($courseids)) {
-        // nothing to do
+        // Nothing to do.
         return;
     }
 
-    if (!$category = $DB->get_record('course_categories', array('id'=>$categoryid))) {
+    if (!$category = $DB->get_record('course_categories', array('id' => $categoryid))) {
         return false;
     }
 
@@ -2042,21 +2043,37 @@ function move_courses($courseids, $categoryid) {
     $i = 1;
 
     foreach ($courseids as $courseid) {
-        if ($course = $DB->get_record('course', array('id'=>$courseid), 'id, category')) {
+        if ($dbcourse = $DB->get_record('course', array('id' => $courseid))) {
             $course = new stdClass();
             $course->id = $courseid;
             $course->category  = $category->id;
             $course->sortorder = $category->sortorder + MAX_COURSES_IN_CATEGORY - $i++;
             if ($category->visible == 0) {
-                // hide the course when moving into hidden category,
-                // do not update the visibleold flag - we want to get to previous state if somebody unhides the category
+                // Hide the course when moving into hidden category, do not update the visibleold flag - we want to get
+                // to previous state if somebody unhides the category.
                 $course->visible = 0;
             }
 
             $DB->update_record('course', $course);
-            add_to_log($course->id, "course", "move", "edit.php?id=$course->id", $course->id);
 
-            $context   = context_course::instance($course->id);
+            // Store the context.
+            $context = context_course::instance($course->id);
+
+            // Update the course object we are passing to the event.
+            $dbcourse->category = $course->category;
+            $dbcourse->sortorder = $course->sortorder;
+
+            // Trigger a course updated event.
+            $event = \core\event\course_updated::create(array(
+                'objectid' => $course->id,
+                'context' => $context,
+                'other' => array('shortname' => $dbcourse->shortname,
+                                 'fullname' => $dbcourse->fullname)
+            ));
+            $event->add_record_snapshot('course', $dbcourse);
+            $event->set_legacy_logdata(array($course->id, 'course', 'move', 'edit.php?id=' . $course->id, $course->id));
+            $event->trigger();
+
             $context->update_moved($newparent);
         }
     }
@@ -2229,7 +2246,7 @@ function course_overviewfiles_options($course) {
  * @return object new course instance
  */
 function create_course($data, $editoroptions = NULL) {
-    global $CFG, $DB;
+    global $DB;
 
     //check the categoryid - must be given for all new courses
     $category = $DB->get_record('course_categories', array('id'=>$data->category), '*', MUST_EXIST);
@@ -2304,10 +2321,15 @@ function create_course($data, $editoroptions = NULL) {
     // set up enrolments
     enrol_course_updated(true, $course, $data);
 
-    add_to_log(SITEID, 'course', 'new', 'view.php?id='.$course->id, $data->fullname.' (ID '.$course->id.')');
-
-    // Trigger events
-    events_trigger('course_created', $course);
+    // Trigger a course created event.
+    $event = \core\event\course_created::create(array(
+        'objectid' => $course->id,
+        'context' => context_course::instance($course->id),
+        'other' => array('shortname' => $course->shortname,
+                         'fullname' => $course->fullname)
+    ));
+    $event->add_record_snapshot('course', $course);
+    $event->trigger();
 
     return $course;
 }
@@ -2323,7 +2345,7 @@ function create_course($data, $editoroptions = NULL) {
  * @return void
  */
 function update_course($data, $editoroptions = NULL) {
-    global $CFG, $DB;
+    global $DB;
 
     $data->timemodified = time();
 
@@ -2393,10 +2415,16 @@ function update_course($data, $editoroptions = NULL) {
     // update enrol settings
     enrol_course_updated(false, $course, $data);
 
-    add_to_log($course->id, "course", "update", "edit.php?id=$course->id", $course->id);
-
-    // Trigger events
-    events_trigger('course_updated', $course);
+    // Trigger a course updated event.
+    $event = \core\event\course_updated::create(array(
+        'objectid' => $course->id,
+        'context' => $context,
+        'other' => array('shortname' => $course->shortname,
+                         'fullname' => $course->fullname)
+    ));
+    $event->add_record_snapshot('course', $course);
+    $event->set_legacy_logdata(array($course->id, 'course', 'update', 'edit.php?id=' . $course->id, $course->id));
+    $event->trigger();
 
     if ($oldcourse->format !== $course->format) {
         // Remove all options stored for the previous format
@@ -2898,7 +2926,7 @@ function course_ajax_enabled($course) {
  * @return bool
  */
 function include_course_ajax($course, $usedmodules = array(), $enabledmodules = null, $config = null) {
-    global $PAGE, $SITE;
+    global $CFG, $PAGE, $SITE;
 
     // Ensure that ajax should be included
     if (!course_ajax_enabled($course)) {
@@ -2977,6 +3005,9 @@ function include_course_ajax($course, $usedmodules = array(), $enabledmodules =
             'markedthistopic',
             'move',
             'movesection',
+            'movecontent',
+            'tocontent',
+            'emptydragdropregion'
         ), 'moodle');
 
     // Include format-specific strings
@@ -2993,6 +3024,7 @@ function include_course_ajax($course, $usedmodules = array(), $enabledmodules =
     }
 
     // Load drag and drop upload AJAX.
+    require_once($CFG->dirroot.'/course/dnduploadlib.php');
     dndupload_add_to_course($course, $enabledmodules);
 
     return true;
index 0c1ec26..97f7d2a 100644 (file)
@@ -1,5 +1,5 @@
 <?php
-// Allows a teacher/admin to login as another user (in stealth mode)
+// Allows a teacher/admin to login as another user (in stealth mode).
 
 require_once('../config.php');
 require_once('lib.php');
@@ -10,7 +10,7 @@ $redirect = optional_param('redirect', 0, PARAM_BOOL);
 $url = new moodle_url('/course/loginas.php', array('id'=>$id));
 $PAGE->set_url($url);
 
-/// Reset user back to their real self if needed, for security reasons you need to log out and log in again
+// Reset user back to their real self if needed, for security reasons you need to log out and log in again.
 if (session_is_loggedinas()) {
     require_sesskey();
     require_logout();
@@ -29,15 +29,13 @@ if ($redirect) {
     redirect(get_login_url());
 }
 
-///-------------------------------------
-/// We are trying to log in as this user in the first place
-
-$userid = required_param('user', PARAM_INT);         // login as this user
+// Try log in as this user.
+$userid = required_param('user', PARAM_INT);
 
 require_sesskey();
 $course = $DB->get_record('course', array('id'=>$id), '*', MUST_EXIST);
 
-/// User must be logged in
+// User must be logged in.
 
 $systemcontext = context_system::instance();
 $coursecontext = context_course::instance($course->id);
@@ -62,13 +60,10 @@ if (has_capability('moodle/user:loginas', $systemcontext)) {
     $context = $coursecontext;
 }
 
-/// Login as this user and return to course home page.
-$oldfullname = fullname($USER, true);
+// Login as this user and return to course home page.
 session_loginas($userid, $context);
 $newfullname = fullname($USER, true);
 
-add_to_log($course->id, "course", "loginas", "../user/view.php?id=$course->id&amp;user=$userid", "$oldfullname -> $newfullname");
-
 $strloginas    = get_string('loginas');
 $strloggedinas = get_string('loggedinas', '', $newfullname);
 
index d4a9054..4888c0a 100644 (file)
@@ -249,7 +249,22 @@ if ((!empty($hide) or !empty($show)) && confirm_sesskey()) {
     $params = array('id' => $course->id, 'visible' => $visible, 'visibleold' => $visible, 'timemodified' => time());
     $DB->update_record('course', $params);
     cache_helper::purge_by_event('changesincourse');
-    add_to_log($course->id, "course", ($visible ? 'show' : 'hide'), "edit.php?id=$course->id", $course->id);
+
+    // Update the course object we pass to the event class.
+    $course->visible = $params['visible'];
+    $course->visibleold = $params['visibleold'];
+    $course->timemodified = $params['timemodified'];
+
+    // Trigger a course updated event.
+    $event = \core\event\course_updated::create(array(
+        'objectid' => $course->id,
+        'context' => $coursecontext,
+        'other' => array('shortname' => $course->shortname,
+                         'fullname' => $course->fullname)
+    ));
+    $event->add_record_snapshot('course', $course);
+    $event->set_legacy_logdata(array($course->id, 'course', ($visible ? 'show' : 'hide'), 'edit.php?id=' . $course->id, $course->id));
+    $event->trigger();
 }
 
 if ((!empty($moveup) or !empty($movedown)) && confirm_sesskey()) {
@@ -277,7 +292,20 @@ if ((!empty($moveup) or !empty($movedown)) && confirm_sesskey()) {
         $DB->set_field('course', 'sortorder', $swapcourse->sortorder, array('id' => $movecourse->id));
         $DB->set_field('course', 'sortorder', $movecourse->sortorder, array('id' => $swapcourse->id));
         cache_helper::purge_by_event('changesincourse');
-        add_to_log($movecourse->id, "course", "move", "edit.php?id=$movecourse->id", $movecourse->id);
+
+        // Update $movecourse's sortorder.
+        $movecourse->sortorder = $swapcourse->sortorder;
+
+        // Trigger a course updated event.
+        $event = \core\event\course_updated::create(array(
+            'objectid' => $movecourse->id,
+            'context' => context_course::instance($movecourse->id),
+            'other' => array('shortname' => $movecourse->shortname,
+                             'fullname' => $movecourse->fullname)
+        ));
+        $event->add_record_snapshot('course', $movecourse);
+        $event->set_legacy_logdata(array($movecourse->id, 'course', 'move', 'edit.php?id=' . $movecourse->id, $movecourse->id));
+        $event->trigger();
     }
 }
 
index b805289..01c4bfc 100644 (file)
@@ -1334,4 +1334,370 @@ class core_course_courselib_testcase extends advanced_testcase {
         $eventcount = $DB->count_records('event', array('instance' => $assign->id, 'modulename' => 'assign'));
         $this->assertEmpty($eventcount);
     }
+
+    /**
+     * Test that triggering a course_created event works as expected.
+     */
+    public function test_course_created_event() {
+        $this->resetAfterTest();
+
+        // Catch the events.
+        $sink = $this->redirectEvents();
+
+        // Create the course.
+        $course = $this->getDataGenerator()->create_course();
+
+        // Capture the event.
+        $events = $sink->get_events();
+        $sink->close();
+
+        // Validate the event.
+        $event = $events[0];
+        $this->assertInstanceOf('\core\event\course_created', $event);
+        $this->assertEquals('course', $event->objecttable);
+        $this->assertEquals($course->id, $event->objectid);
+        $this->assertEquals(context_course::instance($course->id)->id, $event->contextid);
+        $this->assertEquals($course, $event->get_record_snapshot('course', $course->id));
+        $this->assertEquals('course_created', $event->get_legacy_eventname());
+        $this->assertEventLegacyData($course, $event);
+        $expectedlog = array(SITEID, 'course', 'new', 'view.php?id=' . $course->id, $course->fullname . ' (ID ' . $course->id . ')');
+        $this->assertEventLegacyLogData($expectedlog, $event);
+    }
+
+    /**
+     * Test that triggering a course_updated event works as expected.
+     */
+    public function test_course_updated_event() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Create a course.
+        $course = $this->getDataGenerator()->create_course();
+
+        // Create a category we are going to move this course to.
+        $category = $this->getDataGenerator()->create_category();
+
+        // Catch the update events.
+        $sink = $this->redirectEvents();
+
+        // Keep track of the old sortorder.
+        $sortorder = $course->sortorder;
+
+        // Call update_course which will trigger a course_updated event.
+        update_course($course);
+
+        // Return the updated course information from the DB.
+        $updatedcourse = $DB->get_record('course', array('id' => $course->id), '*', MUST_EXIST);
+
+        // Now move the course to the category, this will also trigger an event.
+        move_courses(array($course->id), $category->id);
+
+        // Return the moved course information from the DB.
+        $movedcourse = $DB->get_record('course', array('id' => $course->id), '*', MUST_EXIST);
+
+        // Now we want to set the sortorder back to what it was before fix_course_sortorder() was called. The reason for
+        // this is because update_course() and move_courses() call fix_course_sortorder() which alters the sort order in
+        // the DB, but it does not set the value of the sortorder for the course object passed to the event.
+        $updatedcourse->sortorder = $sortorder;
+        $movedcourse->sortorder = $category->sortorder + MAX_COURSES_IN_CATEGORY - 1;
+
+        // Capture the events.
+        $events = $sink->get_events();
+        $sink->close();
+
+        // Validate the events.
+        $event = $events[0];
+        $this->assertInstanceOf('\core\event\course_updated', $event);
+        $this->assertEquals('course', $event->objecttable);
+        $this->assertEquals($updatedcourse->id, $event->objectid);
+        $this->assertEquals(context_course::instance($updatedcourse->id)->id, $event->contextid);
+        $this->assertEquals($updatedcourse, $event->get_record_snapshot('course', $updatedcourse->id));
+        $this->assertEquals('course_updated', $event->get_legacy_eventname());
+        $this->assertEventLegacyData($updatedcourse, $event);
+        $expectedlog = array($updatedcourse->id, 'course', 'update', 'edit.php?id=' . $course->id, $course->id);
+        $this->assertEventLegacyLogData($expectedlog, $event);
+
+        $event = $events[1];
+        $this->assertInstanceOf('\core\event\course_updated', $event);
+        $this->assertEquals('course', $event->objecttable);
+        $this->assertEquals($movedcourse->id, $event->objectid);
+        $this->assertEquals(context_course::instance($movedcourse->id)->id, $event->contextid);
+        $this->assertEquals($movedcourse, $event->get_record_snapshot('course', $movedcourse->id));
+        $this->assertEquals('course_updated', $event->get_legacy_eventname());
+        $this->assertEventLegacyData($movedcourse, $event);
+        $expectedlog = array($movedcourse->id, 'course', 'move', 'edit.php?id=' . $movedcourse->id, $movedcourse->id);
+        $this->assertEventLegacyLogData($expectedlog, $event);
+    }
+
+    /**
+     * Test that triggering a course_deleted event works as expected.
+     */
+    public function test_course_deleted_event() {
+        $this->resetAfterTest();
+
+        // Create the course.
+        $course = $this->getDataGenerator()->create_course();
+
+        // Save the course context before we delete the course.
+        $coursecontext = context_course::instance($course->id);
+
+        // Catch the update event.
+        $sink = $this->redirectEvents();
+
+        // Call delete_course() which will trigger the course_deleted event and the course_content_deleted
+        // event. This function prints out data to the screen, which we do not want during a PHPUnit test,
+        // so use ob_start and ob_end_clean to prevent this.
+        ob_start();
+        delete_course($course);
+        ob_end_clean();
+
+        // Capture the event.
+        $events = $sink->get_events();
+        $sink->close();
+
+        // Validate the event.
+        $event = $events[1];
+        $this->assertInstanceOf('\core\event\course_deleted', $event);
+        $this->assertEquals('course', $event->objecttable);
+        $this->assertEquals($course->id, $event->objectid);
+        $this->assertEquals($coursecontext->id, $event->contextid);
+        $this->assertEquals($course, $event->get_record_snapshot('course', $course->id));
+        $this->assertEquals('course_deleted', $event->get_legacy_eventname());
+        // The legacy data also passed the context in the course object.
+        $course->context = $coursecontext;
+        $this->assertEventLegacyData($course, $event);
+        $expectedlog = array(SITEID, 'course', 'delete', 'view.php?id=' . $course->id, $course->fullname . '(ID ' . $course->id . ')');
+        $this->assertEventLegacyLogData($expectedlog, $event);
+    }
+
+    /**
+     * Test that triggering a course_content_deleted event works as expected.
+     */
+    public function test_course_content_deleted_event() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Create the course.
+        $course = $this->getDataGenerator()->create_course();
+
+        // Get the course from the DB. The data generator adds some extra properties, such as
+        // numsections, to the course object which will fail the assertions later on.
+        $course = $DB->get_record('course', array('id' => $course->id), '*', MUST_EXIST);
+
+        // Save the course context before we delete the course.
+        $coursecontext = context_course::instance($course->id);
+
+        // Catch the update event.
+        $sink = $this->redirectEvents();
+
+        // Call remove_course_contents() which will trigger the course_content_deleted event.
+        // This function prints out data to the screen, which we do not want during a PHPUnit
+        // test, so use ob_start and ob_end_clean to prevent this.
+        ob_start();
+        remove_course_contents($course->id);
+        ob_end_clean();
+
+        // Capture the event.
+        $events = $sink->get_events();
+        $sink->close();
+
+        // Validate the event.
+        $event = $events[0];
+        $this->assertInstanceOf('\core\event\course_content_deleted', $event);
+        $this->assertEquals('course', $event->objecttable);
+        $this->assertEquals($course->id, $event->objectid);
+        $this->assertEquals($coursecontext->id, $event->contextid);
+        $this->assertEquals($course, $event->get_record_snapshot('course', $course->id));
+        $this->assertEquals('course_content_removed', $event->get_legacy_eventname());
+        // The legacy data also passed the context and options in the course object.
+        $course->context = $coursecontext;
+        $course->options = array();
+        $this->assertEventLegacyData($course, $event);
+    }
+
+    /**
+     * Test that triggering a course_category_deleted event works as expected.
+     */
+    public function test_course_category_deleted_event() {
+        $this->resetAfterTest();
+
+        // Create a category.
+        $category = $this->getDataGenerator()->create_category();
+
+        // Save the context before it is deleted.
+        $categorycontext = context_coursecat::instance($category->id);
+
+        // Catch the update event.
+        $sink = $this->redirectEvents();
+
+        // Delete the category.
+        $category->delete_full();
+
+        // Capture the event.
+        $events = $sink->get_events();
+        $sink->close();
+
+        // Validate the event.
+        $event = $events[0];
+        $this->assertInstanceOf('\core\event\course_category_deleted', $event);
+        $this->assertEquals('course_categories', $event->objecttable);
+        $this->assertEquals($category->id, $event->objectid);
+        $this->assertEquals($categorycontext->id, $event->contextid);
+        $this->assertEquals('course_category_deleted', $event->get_legacy_eventname());
+        $this->assertEventLegacyData($category, $event);
+        $expectedlog = array(SITEID, 'category', 'delete', 'index.php', $category->name . '(ID ' . $category->id . ')');
+        $this->assertEventLegacyLogData($expectedlog, $event);
+
+        // Create two categories.
+        $category = $this->getDataGenerator()->create_category();
+        $category2 = $this->getDataGenerator()->create_category();
+
+        // Save the context before it is moved and then deleted.
+        $category2context = context_coursecat::instance($category2->id);
+
+        // Catch the update event.
+        $sink = $this->redirectEvents();
+
+        // Move the category.
+        $category2->delete_move($category->id);
+
+        // Capture the event.
+        $events = $sink->get_events();
+        $sink->close();
+
+        // Validate the event.
+        $event = $events[0];
+        $this->assertInstanceOf('\core\event\course_category_deleted', $event);
+        $this->assertEquals('course_categories', $event->objecttable);
+        $this->assertEquals($category2->id, $event->objectid);
+        $this->assertEquals($category2context->id, $event->contextid);
+        $this->assertEquals('course_category_deleted', $event->get_legacy_eventname());
+        $this->assertEventLegacyData($category2, $event);
+        $expectedlog = array(SITEID, 'category', 'delete', 'index.php', $category2->name . '(ID ' . $category2->id . ')');
+        $this->assertEventLegacyLogData($expectedlog, $event);
+    }
+
+    /**
+     * Test that triggering a course_restored event works as expected.
+     */
+    public function test_course_restored_event() {
+        global $CFG;
+
+        // Get the necessary files to perform backup and restore.
+        require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
+        require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
+
+        $this->resetAfterTest();
+
+        // Set to admin user.
+        $this->setAdminUser();
+
+        // The user id is going to be 2 since we are the admin user.
+        $userid = 2;
+
+        // Create a course.
+        $course = $this->getDataGenerator()->create_course();
+
+        // Create backup file and save it to the backup location.
+        $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE,
+            backup::INTERACTIVE_NO, backup::MODE_GENERAL, $userid);
+        $bc->execute_plan();
+        $results = $bc->get_results();
+        $file = $results['backup_destination'];
+        $fp = get_file_packer();
+        $filepath = $CFG->dataroot . '/temp/backup/test-restore-course-event';
+        $file->extract_to_pathname($fp, $filepath);
+        $bc->destroy();
+        unset($bc);
+
+        // Now we want to catch the restore course event.
+        $sink = $this->redirectEvents();
+
+        // Now restore the course to trigger the event.
+        $rc = new restore_controller('test-restore-course-event', $course->id, backup::INTERACTIVE_NO,
+            backup::MODE_GENERAL, $userid, backup::TARGET_NEW_COURSE);
+        $rc->execute_precheck();
+        $rc->execute_plan();
+
+        // Capture the event.
+        $events = $sink->get_events();
+        $sink->close();
+
+        // Validate the event.
+        $event = $events[0];
+        $this->assertInstanceOf('\core\event\course_restored', $event);
+        $this->assertEquals('course', $event->objecttable);
+        $this->assertEquals($rc->get_courseid(), $event->objectid);
+        $this->assertEquals(context_course::instance($rc->get_courseid())->id, $event->contextid);
+        $this->assertEquals('course_restored', $event->get_legacy_eventname());
+        $legacydata = (object) array(
+            'courseid' => $rc->get_courseid(),
+            'userid' => $rc->get_userid(),
+            'type' => $rc->get_type(),
+            'target' => $rc->get_target(),
+            'mode' => $rc->get_mode(),
+            'operation' => $rc->get_operation(),
+            'samesite' => $rc->is_samesite()
+        );
+        $this->assertEventLegacyData($legacydata, $event);
+
+        // Destroy the resource controller since we are done using it.
+        $rc->destroy();
+        unset($rc);
+
+        // Clear the time limit, otherwise PHPUnit complains.
+        set_time_limit(0);
+    }
+
+    /**
+     * Test that triggering a course_section_updated event works as expected.
+     */
+    public function test_course_section_updated_event() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Create the course with sections.
+        $course = $this->getDataGenerator()->create_course(array('numsections' => 10), array('createsections' => true));
+        $sections = $DB->get_records('course_sections', array('course' => $course->id));
+
+        $coursecontext = context_course::instance($course->id);
+
+        $section = array_pop($sections);
+        $section->name = 'Test section';
+        $section->summary = 'Test section summary';
+        $DB->update_record('course_sections', $section);
+
+        // Trigger an event for course section update.
+        $event = \core\event\course_section_updated::create(
+                array(
+                    'objectid' => $section->id,
+                    'courseid' => $course->id,
+                    'context' => context_course::instance($course->id)
+                )
+            );
+        $event->add_record_snapshot('course_sections', $section);
+        // Trigger and catch event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $sink->close();
+
+        // Validate the event.
+        $event = $events[0];
+        $this->assertInstanceOf('\core\event\course_section_updated', $event);
+        $this->assertEquals('course_sections', $event->objecttable);
+        $this->assertEquals($section->id, $event->objectid);
+        $this->assertEquals($course->id, $event->courseid);
+        $this->assertEquals($coursecontext->id, $event->contextid);
+        $expecteddesc = 'Course ' . $event->courseid . ' section ' . $event->other['sectionnum'] . ' updated by user ' . $event->userid;
+        $this->assertEquals($expecteddesc, $event->get_description());
+        $this->assertEquals($section, $event->get_record_snapshot('course_sections', $event->objectid));
+        $id = $section->id;
+        $sectionnum = $section->section;
+        $expectedlegacydata = array($course->id, "course", "editsection", 'editsection.php?id=' . $id, $sectionnum);
+        $this->assertEventLegacyLogData($expectedlegacydata, $event);
+    }
 }
index cf3748a..5430b6c 100644 (file)
         redirect($CFG->wwwroot .'/');
     }
 
+    $ajaxenabled = ajaxenabled();
+
     $completion = new completion_info($course);
-    if ($completion->is_enabled() && ajaxenabled()) {
+    if ($completion->is_enabled() && $ajaxenabled) {
         $PAGE->requires->string_for_js('completion-title-manual-y', 'completion');
         $PAGE->requires->string_for_js('completion-title-manual-n', 'completion');
         $PAGE->requires->string_for_js('completion-alt-manual-y', 'completion');
     $PAGE->set_heading($course->fullname);
     echo $OUTPUT->header();
 
-    if ($completion->is_enabled() && ajaxenabled()) {
+    if ($completion->is_enabled() && $ajaxenabled) {
         // This value tracks whether there has been a dynamic change to the page.
         // It is used so that if a user does this - (a) set some tickmarks, (b)
         // go to another page, (c) clicks Back button - the page will
index 0145f0f..fc5f88d 100644 (file)
@@ -295,6 +295,7 @@ YUI.add('moodle-course-dragdrop', function(Y) {
                     resources.addClass(CSS.SECTION);
                     sectionnode.one('.'+CSS.CONTENT+' div.'+CSS.SUMMARY).insert(resources, 'after');
                 }
+                resources.setAttribute('data-draggroups', this.groups.join(' '));
                 // Define empty ul as droptarget, so that item could be moved to empty list
                 var tar = new Y.DD.Drop({
                     node: resources,
index 4add14e..e0117b9 100644 (file)
@@ -311,8 +311,8 @@ class enrol_flatfile_plugin extends enrol_plugin {
                 }
                 $roleid = $rolemap[$fields[1]];
 
-                if (empty($fields[2]) or !$user = $DB->get_record("user", array("idnumber"=>$fields[2]))) {
-                    $trace->output("Unknown user idnumber in field 3 - ignoring line $line", 1);
+                if (empty($fields[2]) or !$user = $DB->get_record("user", array("idnumber"=>$fields[2], 'deleted'=>0))) {
+                    $trace->output("Unknown user idnumber or deleted user in field 3 - ignoring line $line", 1);
                     continue;
                 }
 
index e43b12c..c1bff98 100644 (file)
@@ -46,7 +46,7 @@ require(__DIR__.'/../../../config.php');
 require_once("$CFG->libdir/clilib.php");
 
 // Ensure errors are well explained.
-$CFG->debug = DEBUG_DEVELOPER;
+set_debugging(DEBUG_DEVELOPER, true);
 
 if (!enrol_is_enabled('ldap')) {
     cli_error(get_string('pluginnotenabled', 'enrol_ldap'), 2);
index 250fcd8..cba40a8 100644 (file)
@@ -250,4 +250,33 @@ class core_enrollib_testcase extends advanced_testcase {
         $this->assertTrue(enrol_user_sees_own_courses());
         $this->assertEquals($reads, $DB->perf_get_reads());
     }
+
+    public function test_enrol_get_shared_courses() {
+        $this->resetAfterTest();
+
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
+
+        $course2 = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course2->id);
+
+        // Test that user1 and user2 have courses in common.
+        $this->assertTrue(enrol_get_shared_courses($user1, $user2, false, true));
+        // Test that user1 and user3 have no courses in common.
+        $this->assertFalse(enrol_get_shared_courses($user1, $user3, false, true));
+
+        // Test retrieving the courses in common.
+        $sharedcourses = enrol_get_shared_courses($user1, $user2, true);
+
+        // Only should be one shared course.
+        $this->assertCount(1, $sharedcourses);
+        $sharedcourse = array_shift($sharedcourses);
+        // It should be course 1.
+        $this->assertEquals($sharedcourse->id, $course1->id);
+    }
 }
index 423ea14..af37580 100644 (file)
@@ -5,4 +5,5 @@
 .enrolpanel .container .header h2 {font-size:90%;text-align:center;margin:5px;}
 .enrolpanel .container .header .close {width:25px;height:15px;position:absolute;top:5px;right:1em;cursor:pointer;background:url("sprite.png") no-repeat scroll 0 0 transparent;}
 .enrolpanel .container .content {}
-.enrolpanel .container .content input {margin:5px;font-size:10px;}
\ No newline at end of file
+.enrolpanel .container .content input {margin:5px;font-size:10px;}
+.enrolpanel.roleassign.visible .container {width:auto;}
index 9080888..6c82cc3 100644 (file)
@@ -381,7 +381,11 @@ YUI.add('moodle-enrol-rolemanager', function(Y) {
             var roles = this.user.get(CONTAINER).one('.col_role .roles');
             var x = roles.getX() + 10;
             var y = roles.getY() + this.user.get(CONTAINER).get('offsetHeight') - 10;
-            this.get('elementNode').setStyle('left', x).setStyle('top', y);
+            if ( Y.one(document.body).hasClass('dir-rtl') ) {
+                this.get('elementNode').setStyle('right', x - 20).setStyle('top', y);
+            } else {
+                this.get('elementNode').setStyle('left', x).setStyle('top', y);
+            }
             this.get('elementNode').addClass('visible');
             this.escCloseEvent = Y.on('key', this.hide, document.body, 'down:27', this);
             this.displayed = true;
index e086c6e..e97478d 100644 (file)
@@ -47,13 +47,16 @@ class core_files_external extends external_api {
     public static function get_files_parameters() {
         return new external_function_parameters(
             array(
-                'contextid' => new external_value(PARAM_INT, 'context id'),
-                'component' => new external_value(PARAM_TEXT, 'component'),
-                'filearea'  => new external_value(PARAM_TEXT, 'file area'),
-                'itemid'    => new external_value(PARAM_INT, 'associated id'),
-                'filepath'  => new external_value(PARAM_PATH, 'file path'),
-                'filename'  => new external_value(PARAM_FILE, 'file name'),
-                'modified' => new external_value(PARAM_INT, 'timestamp to return files changed after this time.', VALUE_DEFAULT, null)
+                'contextid'    => new external_value(PARAM_INT, 'context id Set to -1 to use contextlevel and instanceid.'),
+                'component'    => new external_value(PARAM_TEXT, 'component'),
+                'filearea'     => new external_value(PARAM_TEXT, 'file area'),
+                'itemid'       => new external_value(PARAM_INT, 'associated id'),
+                'filepath'     => new external_value(PARAM_PATH, 'file path'),
+                'filename'     => new external_value(PARAM_FILE, 'file name'),
+                'modified'     => new external_value(PARAM_INT, 'timestamp to return files changed after this time.', VALUE_DEFAULT, null),
+                'contextlevel' => new external_value(PARAM_ALPHA, 'The context level for the file location.', VALUE_DEFAULT, null),
+                'instanceid'   => new external_value(PARAM_INT, 'The instance id for where the file is located.', VALUE_DEFAULT, null)
+
             )
         );
     }
@@ -68,22 +71,41 @@ class core_files_external extends external_api {
      * @param string $filepath file path
      * @param string $filename file name
      * @param int $modified timestamp to return files changed after this time.
+     * @param string $contextlevel The context level for the file location.
+     * @param int $instanceid The instance id for where the file is located.
      * @return array
      * @since Moodle 2.2
      */
-    public static function get_files($contextid, $component, $filearea, $itemid, $filepath, $filename, $modified = null) {
-        global $CFG, $USER, $OUTPUT;
-        $fileinfo = self::validate_parameters(self::get_files_parameters(), array(
-                    'contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea,
-                    'itemid'=>$itemid, 'filepath'=>$filepath, 'filename'=>$filename, 'modified'=>$modified));
+    public static function get_files($contextid, $component, $filearea, $itemid, $filepath, $filename, $modified = null,
+                                     $contextlevel = null, $instanceid = null) {
+
+        $parameters = array(
+            'contextid'    => $contextid,
+            'component'    => $component,
+            'filearea'     => $filearea,
+            'itemid'       => $itemid,
+            'filepath'     => $filepath,
+            'filename'     => $filename,
+            'modified'     => $modified,
+            'contextlevel' => $contextlevel,
+            'instanceid'   => $instanceid);
+        $fileinfo = self::validate_parameters(self::get_files_parameters(), $parameters);
 
         $browser = get_file_browser();
 
-        if (empty($fileinfo['contextid'])) {
-            $context  = context_system::instance();
+        // We need to preserve backwards compatibility. Zero will use the system context and minus one will
+        // use the addtional parameters to determine the context.
+        // TODO MDL-40489 get_context_from_params should handle this logic.
+        if ($fileinfo['contextid'] == 0) {
+            $context = context_system::instance();
         } else {
-            $context  = context::instance_by_id($fileinfo['contextid']);
+            if ($fileinfo['contextid'] == -1) {
+                $fileinfo['contextid'] = null;
+            }
+            $context = self::get_context_from_params($fileinfo);
         }
+        self::validate_context($context);
+
         if (empty($fileinfo['component'])) {
             $fileinfo['component'] = null;
         }
@@ -104,6 +126,7 @@ class core_files_external extends external_api {
         $return['parents'] = array();
         $return['files'] = array();
         $list = array();
+
         if ($file = $browser->get_file_info(
             $context, $fileinfo['component'], $fileinfo['filearea'], $fileinfo['itemid'],
                 $fileinfo['filepath'], $fileinfo['filename'])) {
index 7f6de7d..4e21d91 100644 (file)
@@ -175,4 +175,121 @@ class core_files_externallib_testcase extends advanced_testcase {
         $file = $browser->get_file_info($context, $component, $filearea, $itemid, $filepath, $filename);
         $this->assertNotEmpty($file);
     }
+
+    public function test_get_files() {
+        global $USER, $DB;
+
+        $this->resetAfterTest();
+
+        $this->setAdminUser();
+        $USER->email = 'test@moodle.com';
+
+        $course = $this->getDataGenerator()->create_course();
+        $record = new stdClass();
+        $record->course = $course->id;
+        $record->name = "Mod data upload test";
+
+        $record->intro = "Some intro of some sort";
+
+        $module = $this->getDataGenerator()->create_module('data', $record);
+
+        $field = data_get_field_new('file', $module);
+
+        $fielddetail = new stdClass();
+        $fielddetail->d = $module->id;
+        $fielddetail->mode = 'add';
+        $fielddetail->type = 'file';
+        $fielddetail->sesskey = sesskey();
+        $fielddetail->name = 'Upload file';
+        $fielddetail->description = 'Some description';
+        $fielddetail->param3 = '0';
+
+        $field->define_field($fielddetail);
+        $field->insert_field();
+        $recordid = data_add_record($module);
+
+        $timemodified = $DB->get_field('data_records', 'timemodified', array('id' => $recordid));
+
+        $datacontent = array();
+        $datacontent['fieldid'] = $field->field->id;
+        $datacontent['recordid'] = $recordid;
+        $datacontent['content'] = 'Simple4.txt';
+
+        $contentid = $DB->insert_record('data_content', $datacontent);
+
+        $context = context_module::instance($module->id);
+        $usercontext = context_user::instance($USER->id);
+        $component = 'mod_data';
+        $filearea = 'content';
+        $itemid = $contentid;
+        $filename = $datacontent['content'];
+        $filecontent = base64_encode("Let us create a nice simple file.");
+
+        $filerecord = array();
+        $filerecord['contextid'] = $context->id;
+        $filerecord['component'] = $component;
+        $filerecord['filearea'] = $filearea;
+        $filerecord['itemid'] = $itemid;
+        $filerecord['filepath'] = '/';
+        $filerecord['filename'] = $filename;
+
+        $fs = get_file_storage();
+        $file = $fs->create_file_from_string($filerecord, $filecontent);
+
+        $filename = '';
+        $testfilelisting = core_files_external::get_files($context->id, $component, $filearea, $itemid, '/', $filename);
+
+        $testdata = array();
+        $testdata['parents'] = array();
+        $testdata['parents']['0'] = array('contextid' => 1,
+                                          'component' => null,
+                                          'filearea' => null,
+                                          'itemid' => null,
+                                          'filepath' => null,
+                                          'filename' => 'System');
+        $testdata['parents']['1'] = array('contextid' => 3,
+                                          'component' => null,
+                                          'filearea' => null,
+                                          'itemid' => null,
+                                          'filepath' => null,
+                                          'filename' => 'Miscellaneous');
+        $testdata['parents']['2'] = array('contextid' => 15,
+                                          'component' => null,
+                                          'filearea' => null,
+                                          'itemid' => null,
+                                          'filepath' => null,
+                                          'filename' => 'Test course 1');
+        $testdata['parents']['3'] = array('contextid' => 20,
+                                          'component' => null,
+                                          'filearea' => null,
+                                          'itemid' => null,
+                                          'filepath' => null,
+                                          'filename' => 'Mod data upload test (Database)');
+        $testdata['parents']['4'] = array('contextid' => 20,
+                                          'component' => 'mod_data',
+                                          'filearea' => 'content',
+                                          'itemid' => null,
+                                          'filepath' => null,
+                                          'filename' => 'Fields');
+        $testdata['files'] = array();
+        $testdata['files']['0'] = array('contextid' => 20,
+                                        'component' => 'mod_data',
+                                        'filearea' => 'content',
+                                        'itemid' => 1,
+                                        'filepath' => '/',
+                                        'filename' => 'Simple4.txt',
+                                        'url' => 'http://www.example.com/moodle/pluginfile.php/20/mod_data/content/1/Simple4.txt',
+                                        'isdir' => null,
+                                        'timemodified' => $timemodified);
+
+        $this->assertEquals($testfilelisting, $testdata);
+
+        // Try again but without the context.
+        $nocontext = -1;
+        $modified = 0;
+        $contextlevel = 'module';
+        $instanceid = $module->id;
+        $testfilelisting = core_files_external::get_files($nocontext, $component, $filearea, $itemid, '/', $filename, $modified, $contextlevel, $instanceid);
+        $this->assertEquals($testfilelisting, $testdata);
+    }
 }
index 35b22bf..5a15eae 100644 (file)
@@ -81,6 +81,7 @@
 .gradingform_rubric .plainvalue.empty {font-style: italic; color: #AAA;}
 
 .gradingform_rubric.editor .criterion .levels .level .delete {position:absolute;right:0;}
+.dir-rtl .gradingform_rubric.editor .criterion .levels .level .delete {position: relative;}
 .gradingform_rubric .criterion .levels .level .score {font-style:italic;color:#575;font-weight: bold;margin-top:5px;white-space:nowrap;}
 .gradingform_rubric .criterion .levels .level .score .scorevalue {padding-right:5px;}
 
index 1c6fd33..2e18c8c 100644 (file)
@@ -1582,15 +1582,15 @@ class grade_report_grader extends grade_report {
      * @return bool
      */
     public function is_fixed_students() {
-        global $USER, $CFG;
+        global $CFG;
         return $CFG->grade_report_fixedstudents &&
-            (check_browser_version('MSIE', '7.0') ||
-             check_browser_version('Firefox', '2.0') ||
-             check_browser_version('Gecko', '2006010100') ||
-             check_browser_version('Camino', '1.0') ||
-             check_browser_version('Opera', '6.0') ||
-             check_browser_version('Chrome', '6') ||
-             check_browser_version('Safari', '300'));
+            (core_useragent::check_ie_version('7.0') ||
+             core_useragent::check_firefox_version('2.0') ||
+             core_useragent::check_gecko_version('2006010100') ||
+             core_useragent::check_camino_version('1.0') ||
+             core_useragent::check_opera_version('6.0') ||
+             core_useragent::check_chrome_version('6') ||
+             core_useragent::check_safari_version('300'));
     }
 
     /**
index ffd8f74..1511619 100644 (file)
@@ -152,7 +152,8 @@ $currenttab = 'groups';
 require('tabs.php');
 
 $disabled = 'disabled="disabled"';
-if (ajaxenabled()) {
+$ajaxenabled = ajaxenabled();
+if ($ajaxenabled) {
     // Some buttons are enabled if single group selected
     $showaddmembersform_disabled = $singlegroup ? '' : $disabled;
     $showeditgroupsettingsform_disabled = $singlegroup ? '' : $disabled;
@@ -178,7 +179,7 @@ echo '<tr>'."\n";
 echo "<td>\n";
 echo '<p><label for="groups"><span id="groupslabel">'.get_string('groups').':</span><span id="thegrouping">&nbsp;</span></label></p>'."\n";
 
-if (ajaxenabled()) { // TODO: move this to JS init!
+if ($ajaxenabled) { // TODO: move this to JS init!
     $onchange = 'M.core_group.membersCombo.refreshMembers();';
 } else {
     $onchange = '';
@@ -275,7 +276,7 @@ echo '</table>'."\n";
 echo '</div>'."\n";
 echo '</form>'."\n";
 
-if (ajaxenabled()) {
+if ($ajaxenabled) {
     $PAGE->requires->js_init_call('M.core_group.init_index', array($CFG->wwwroot, $courseid));
     $PAGE->requires->js_init_call('M.core_group.groupslist', array($preventgroupremoval));
 }
index 7b5fa23..899ebd3 100644 (file)
@@ -181,6 +181,9 @@ $CFG->umaskpermissions     = (($CFG->directorypermissions & 0777) ^ 0777);
 $CFG->running_installer    = true;
 $CFG->early_install_lang   = true;
 $CFG->ostype               = (stristr(PHP_OS, 'win') && !stristr(PHP_OS, 'darwin')) ? 'WINDOWS' : 'UNIX';
+$CFG->debug                = (E_ALL | E_STRICT);
+$CFG->debugdisplay         = true;
+$CFG->debugdeveloper       = true;
 
 // Require all needed libs
 require_once($CFG->libdir.'/setuplib.php');
index 8533af9..ac23770 100644 (file)
@@ -82,6 +82,7 @@ $string['errorminpasswordnonalphanum'] = 'Passwords must have at least {$a} non-
 $string['errorminpasswordupper'] = 'Passwords must have at least {$a} upper case letter(s).';
 $string['errorpasswordupdate'] = 'Error updating password, password not changed';
 $string['event_user_loggedin'] = 'User has logged in';
+$string['eventuserloggedinas'] = 'User logged in as another user';
 $string['forcechangepassword'] = 'Force change password';
 $string['forcechangepasswordfirst_help'] = 'Force users to change password on their first login to Moodle.';
 $string['forcechangepassword_help'] = 'Force users to change password on their next login to Moodle.';
index ec04a5c..44e6599 100644 (file)
@@ -45,6 +45,11 @@ $string['delconfirm'] = 'Do you really want to delete cohort \'{$a}\'?';
 $string['description'] = 'Description';
 $string['duplicateidnumber'] = 'Cohort with the same ID number already exists';
 $string['editcohort'] = 'Edit cohort';
+$string['event_cohort_created'] = 'Cohort created';
+$string['event_cohort_deleted'] = 'Cohort deleted';
+$string['event_cohort_member_added'] = 'User added to a cohort';
+$string['event_cohort_member_removed'] = 'User removed from a cohort';
+$string['event_cohort_updated'] = 'Cohort updated';
 $string['external'] = 'External cohort';
 $string['idnumber'] = 'Cohort ID';
 $string['memberscount'] = 'Cohort size';
index 5fdeed1..56cae77 100644 (file)
@@ -124,6 +124,7 @@ $string['err_nousers'] = 'There are no students on this course or group for whom
 $string['err_settingslocked'] = 'One or more students have already completed a criteria so the settings have been locked. Unlocking the completion criteria settings will delete any existing user data and may cause confusion.';
 $string['err_system'] = 'An internal error occurred in the completion system. (System administrators can enable debugging information to see more detail.)';
 $string['eventcoursecompleted'] = 'Course completed';
+$string['eventcoursecompletionupdated'] = 'Course completion updated';
 $string['eventcoursemodulecompletionupdated'] = 'Course module completion updated';
 $string['excelcsvdownload'] = 'Download in Excel-compatible format (.csv)';
 $string['fraction'] = 'Fraction';
index fdbabdb..2eaabc8 100644 (file)
@@ -645,6 +645,7 @@ $string['emailpasswordsent'] = 'Thank you for confirming the change of password.
 An email containing your new password has been sent to your address at<br /><b>{$a->email}</b>.<br />
 The new password was automatically generated - you might like to
 <a href="{$a->link}">change your password</a> to something easier to remember.';
+$string['emptydragdropregion'] = 'empty region';
 $string['enable'] = 'Enable';
 $string['encryptedcode'] = 'Encrypted code';
 $string['english'] = 'English';
@@ -659,6 +660,13 @@ $string['errorcreatingactivity'] = 'Unable to create an instance of activity \'{
 $string['errorfiletoobig'] = 'The file was bigger than the limit of {$a} bytes';
 $string['errornouploadrepo'] = 'There is no upload repository enabled for this site';
 $string['errorwhenconfirming'] = 'You are not confirmed yet because an error occurred.  If you clicked on a link in an email to get here, make sure that the line in your email wasn\'t broken or wrapped. You may have to use cut and paste to reconstruct the link properly.';
+$string['eventcoursecategorydeleted'] = 'Category deleted';
+$string['eventcoursecontentdeleted'] = 'Course content deleted';
+$string['eventcoursecreated'] = 'Course created';
+$string['eventcoursedeleted'] = 'Course deleted';
+$string['eventcourserestored'] = 'Course restored';
+$string['eventcourseupdated'] = 'Course updated';
+$string['eventcoursesectionupdated'] = ' Course section updated';
 $string['everybody'] = 'Everybody';
 $string['executeat'] = 'Execute at';
 $string['existing'] = 'Existing';
@@ -1080,6 +1088,7 @@ $string['moreinformation'] = 'More information about this error';
 $string['moreprofileinfoneeded'] = 'Please tell us more about yourself';
 $string['mostrecently'] = 'most recently';
 $string['move'] = 'Move';
+$string['movecontent'] = 'Move {$a}';
 $string['movecategorycontentto'] = 'Move into';
 $string['movecategoryto'] = 'Move category to:';
 $string['movecontentstoanothercategory'] = 'Move contents to another category';
@@ -1673,6 +1682,7 @@ $string['time'] = 'Time';
 $string['timezone'] = 'Timezone';
 $string['to'] = 'To';
 $string['tocreatenewaccount'] = 'Skip to create new account';
+$string['tocontent'] = 'To item "{$a}"';
 $string['today'] = 'Today';
 $string['todaylogs'] = 'Today\'s logs';
 $string['toeveryone'] = 'to everyone';
index 2927638..386588a 100644 (file)
@@ -3807,7 +3807,7 @@ function get_users_by_capability(context $context, $capability, $fields = '', $s
             $fields = 'u.*';
         }
     } else {
-        if (debugging('', DEBUG_DEVELOPER) && strpos($fields, 'u.*') === false && strpos($fields, 'u.id') === false) {
+        if ($CFG->debugdeveloper && strpos($fields, 'u.*') === false && strpos($fields, 'u.id') === false) {
             debugging('u.id must be included in the list of fields passed to get_users_by_capability().', DEBUG_DEVELOPER);
         }
     }
index 25ae3d0..5d209bd 100644 (file)
@@ -968,6 +968,8 @@ class admin_category implements parentable_part_of_admin_tree {
      * @return bool True if successfully added, false if $something can not be added.
      */
     public function add($parentname, $something, $beforesibling = null) {
+        global $CFG;
+
         $parent = $this->locate($parentname);
         if (is_null($parent)) {
             debugging('parent does not exist!');
@@ -979,7 +981,7 @@ class admin_category implements parentable_part_of_admin_tree {
                 debugging('error - parts of tree can be inserted only into parentable parts');
                 return false;
             }
-            if (debugging('', DEBUG_DEVELOPER) && !is_null($this->locate($something->name))) {
+            if ($CFG->debugdeveloper && !is_null($this->locate($something->name))) {
                 // The name of the node is already used, simply warn the developer that this should not happen.
                 // It is intentional to check for the debug level before performing the check.
                 debugging('Duplicate admin page name: ' . $something->name, DEBUG_DEVELOPER);
index 4c298b5..daa3126 100644 (file)
@@ -51,7 +51,7 @@ function ajaxenabled(array $browsers = null) {
     if (!empty($browsers)) {
         $valid = false;
         foreach ($browsers as $brand => $version) {
-            if (check_browser_version($brand, $version)) {
+            if (core_useragent::check_browser_version($brand, $version)) {
                 $valid = true;
             }
         }
@@ -61,11 +61,11 @@ function ajaxenabled(array $browsers = null) {
         }
     }
 
-    $ie = check_browser_version('MSIE', 6.0);
-    $ff = check_browser_version('Gecko', 20051106);
-    $op = check_browser_version('Opera', 9.0);
-    $sa = check_browser_version('Safari', 412);
-    $ch = check_browser_version('Chrome', 6);
+    $ie = core_useragent::check_browser_version('MSIE', 6.0);
+    $ff = core_useragent::check_browser_version('Gecko', 20051106);
+    $op = core_useragent::check_browser_version('Opera', 9.0);
+    $sa = core_useragent::check_browser_version('Safari', 412);
+    $ch = core_useragent::check_browser_version('Chrome', 6);
 
     if (!$ie && !$ff && !$op && !$sa && !$ch) {
         /** @see http://en.wikipedia.org/wiki/User_agent */
diff --git a/lib/ajax/tests/ajaxlib_test.php b/lib/ajax/tests/ajaxlib_test.php
deleted file mode 100644 (file)
index f8096f0..0000000
+++ /dev/null
@@ -1,130 +0,0 @@
-<?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/>.
-
-/**
- * Unit tests for (some of) ../ajaxlib.php.
- *
- * @package   core
- * @category  phpunit
- * @copyright 2009 Tim Hunt
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-global $CFG;
-require_once($CFG->libdir . '/ajax/ajaxlib.php');
-
-
-/**
- * Unit tests for ../ajaxlib.php functions.
- *
- * @copyright 2008 Tim Hunt
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class core_ajaxlib_testcase extends advanced_testcase {
-
-    var $user_agents = array(
-        'MSIE' => array(
-            '5.5' => array('Windows 2000' => 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.0)'),
-            '6.0' => array('Windows XP SP2' => 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)'),
-            '7.0' => array('Windows XP SP2' => 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; YPC 3.0.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)')
-        ),
-        'Firefox' => array(
-            '1.0.6'   => array('Windows XP' => 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.10) Gecko/20050716 Firefox/1.0.6'),
-            '1.5'     => array('Windows XP' => 'Mozilla/5.0 (Windows; U; Windows NT 5.1; nl; rv:1.8) Gecko/20051107 Firefox/1.5'),
-            '1.5.0.1' => array('Windows XP' => 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.8.0.1) Gecko/20060111 Firefox/1.5.0.1'),
-            '2.0'     => array('Windows XP' => 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.1) Gecko/20061204 Firefox/2.0.0.1',
-                'Ubuntu Linux AMD64' => 'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.8.1) Gecko/20060601 Firefox/2.0 (Ubuntu-edgy)')
-        ),
-        'Safari' => array(
-            '312' => array('Mac OS X' => 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312'),
-            '2.0' => array('Mac OS X' => 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/412 (KHTML, like Gecko) Safari/412')
-        ),
-        'Opera' => array(
-            '8.51' => array('Windows XP' => 'Opera/8.51 (Windows NT 5.1; U; en)'),
-            '9.0'  => array('Windows XP' => 'Opera/9.0 (Windows NT 5.1; U; en)',
-                'Debian Linux' => 'Opera/9.01 (X11; Linux i686; U; en)')
-        )
-    );
-
-    /**
-     * Uses the array of user agents to test ajax_lib::ajaxenabled
-     */
-    function test_ajaxenabled() {
-        global $CFG;
-
-        $this->resetAfterTest(true);
-
-        $CFG->enableajax = 1;
-
-        // Should be true
-        $_SERVER['HTTP_USER_AGENT'] = $this->user_agents['Firefox']['2.0']['Windows XP'];
-        $this->assertTrue(ajaxenabled());
-
-        $_SERVER['HTTP_USER_AGENT'] = $this->user_agents['Firefox']['1.5']['Windows XP'];
-        $this->assertTrue(ajaxenabled());
-
-        $_SERVER['HTTP_USER_AGENT'] = $this->user_agents['Safari']['2.0']['Mac OS X'];
-        $this->assertTrue(ajaxenabled());
-
-        $_SERVER['HTTP_USER_AGENT'] = $this->user_agents['Opera']['9.0']['Windows XP'];
-        $this->assertTrue(ajaxenabled());
-
-        $_SERVER['HTTP_USER_AGENT'] = $this->user_agents['MSIE']['6.0']['Windows XP SP2'];
-        $this->assertTrue(ajaxenabled());
-
-        // Should be false
-        $_SERVER['HTTP_USER_AGENT'] = $this->user_agents['Firefox']['1.0.6']['Windows XP'];
-        $this->assertFalse(ajaxenabled());
-
-        $_SERVER['HTTP_USER_AGENT'] = $this->user_agents['Safari']['312']['Mac OS X'];
-        $this->assertFalse(ajaxenabled());
-
-        $_SERVER['HTTP_USER_AGENT'] = $this->user_agents['Opera']['8.51']['Windows XP'];
-        $this->assertFalse(ajaxenabled());
-
-        $_SERVER['HTTP_USER_AGENT'] = $this->user_agents['MSIE']['5.5']['Windows 2000'];
-        $this->assertFalse(ajaxenabled());
-
-        // Test array of tested browsers
-        $tested_browsers = array('MSIE' => 6.0, 'Gecko' => 20061111);
-        $_SERVER['HTTP_USER_AGENT'] = $this->user_agents['Firefox']['2.0']['Windows XP'];
-        $this->assertTrue(ajaxenabled($tested_browsers));
-
-        $_SERVER['HTTP_USER_AGENT'] = $this->user_agents['MSIE']['7.0']['Windows XP SP2'];
-        $this->assertTrue(ajaxenabled($tested_browsers));
-
-        $_SERVER['HTTP_USER_AGENT'] = $this->user_agents['Safari']['2.0']['Mac OS X'];
-        $this->assertFalse(ajaxenabled($tested_browsers));
-
-        $_SERVER['HTTP_USER_AGENT'] = $this->user_agents['Opera']['9.0']['Windows XP'];
-        $this->assertFalse(ajaxenabled($tested_browsers));
-
-        $tested_browsers = array('Safari' => 412, 'Opera' => 9.0);
-        $_SERVER['HTTP_USER_AGENT'] = $this->user_agents['Firefox']['2.0']['Windows XP'];
-        $this->assertFalse(ajaxenabled($tested_browsers));
-
-        $_SERVER['HTTP_USER_AGENT'] = $this->user_agents['MSIE']['7.0']['Windows XP SP2'];
-        $this->assertFalse(ajaxenabled($tested_browsers));
-
-        $_SERVER['HTTP_USER_AGENT'] = $this->user_agents['Safari']['2.0']['Mac OS X'];
-        $this->assertTrue(ajaxenabled($tested_browsers));
-
-        $_SERVER['HTTP_USER_AGENT'] = $this->user_agents['Opera']['9.0']['Windows XP'];
-        $this->assertTrue(ajaxenabled($tested_browsers));
-    }
-}
index c8e18af..4901456 100644 (file)
@@ -1025,7 +1025,8 @@ function print_badge_image(badge $badge, stdClass $context, $size = 'small') {
 
     $imageurl = moodle_url::make_pluginfile_url($context->id, 'badges', 'badgeimage', $badge->id, '/', $fsize, false);
     // Appending a random parameter to image link to forse browser reload the image.
-    $attributes = array('src' => $imageurl . '?' . rand(1, 10000), 'alt' => s($badge->name), 'class' => 'activatebadge');
+    $imageurl->param('refresh', rand(1, 10000));
+    $attributes = array('src' => $imageurl, 'alt' => s($badge->name), 'class' => 'activatebadge');
 
     return html_writer::empty_tag('img', $attributes);
 }
index 3f68221..0fa7c30 100644 (file)
@@ -195,12 +195,9 @@ class core_component {
     protected static function is_developer() {
         global $CFG;
 
+        // Note we can not rely on $CFG->debug here because DB is not initialised yet.
         if (isset($CFG->config_php_settings['debug'])) {
-            // Standard moodle script.
             $debug = (int)$CFG->config_php_settings['debug'];
-        } else if (isset($CFG->debug)) {
-            // Usually script with ABORT_AFTER_CONFIG.
-            $debug = (int)$CFG->debug;
         } else {
             return false;
         }
index e206117..a889e3e 100644 (file)
@@ -49,7 +49,7 @@ abstract class assessable_submitted extends \core\event\base {
      */
     protected function init() {
         $this->data['crud'] = 'u';
-        $this->data['level'] = 50;          // TODO MDL-37658.
+        $this->data['level'] = self::LEVEL_PARTICIPATING;
     }
 
     /**
index bfd6a5e..4e4ed8d 100644 (file)
@@ -49,7 +49,7 @@ abstract class assessable_uploaded extends \core\event\base {
      */
     protected function init() {
         $this->data['crud'] = 'c';
-        $this->data['level'] = 50;          // TODO MDL-37658.
+        $this->data['level'] = self::LEVEL_PARTICIPATING;
     }
 
     /**
index 5fcd404..0660cf2 100644 (file)
@@ -50,6 +50,27 @@ namespace core\event;
  * @property-read int $timecreated
  */
 abstract class base implements \IteratorAggregate {
+
+    /**
+     * Other level.
+     */
+    const LEVEL_OTHER = 0;
+
+    /**
+     * Teaching level.
+     *
+     * Any event that is performed by someone (typically a teacher) and has a teaching value,
+     * anything that is affecting the learning experience/environment of the students.
+     */
+    const LEVEL_TEACHING = 1;
+
+    /**
+     * Participating level.
+     *
+     * Any event that is performed by a user, and is related (or could be related) to his learning experience.
+     */
+    const LEVEL_PARTICIPATING = 2;
+
     /** @var array event data */
     protected $data;
 
@@ -108,7 +129,7 @@ abstract class base implements \IteratorAggregate {
      * @throws \coding_exception
      */
     public static final function create(array $data = null) {
-        global $PAGE, $USER;
+        global $PAGE, $USER, $CFG;
 
         $data = (array)$data;
 
@@ -178,7 +199,7 @@ abstract class base implements \IteratorAggregate {
         }
 
         // Warn developers if they do something wrong.
-        if (debugging('', DEBUG_DEVELOPER)) { // This should be replaced by new $CFG->slowdebug flag if introduced.
+        if ($CFG->debugdeveloper) {
             static $automatickeys = array('eventname', 'component', 'action', 'target', 'contextlevel', 'contextinstanceid', 'timecreated');
             static $initkeys = array('crud', 'level', 'objecttable');
 
@@ -187,10 +208,10 @@ abstract class base implements \IteratorAggregate {
                     continue;
 
                 } else if (in_array($key, $automatickeys)) {
-                    debugging("Data key '$key' is not allowed in \\core\\event\\base::create() method, it is set automatically");
+                    debugging("Data key '$key' is not allowed in \\core\\event\\base::create() method, it is set automatically", DEBUG_DEVELOPER);
 
                 } else if (in_array($key, $initkeys)) {
-                    debugging("Data key '$key' is not allowed in \\core\\event\\base::create() method, you need to set it in init() method");
+                    debugging("Data key '$key' is not allowed in \\core\\event\\base::create() method, you need to set it in init() method", DEBUG_DEVELOPER);
 
                 } else if (!in_array($key, self::$fields)) {
                     debugging("Data key '$key' does not exist in \\core\\event\\base");
@@ -208,8 +229,8 @@ abstract class base implements \IteratorAggregate {
      * Override in subclass.
      *
      * Set all required data properties:
-     *  1/ crud - letter [crud]     TODO: MDL-37658
-     *  2/ level - number 1...100   TODO: MDL-37658
+     *  1/ crud - letter [crud]
+     *  2/ level - using a constant self::LEVEL_*.
      *  3/ objecttable - name of database table if objectid specified
      *
      * Optionally it can set:
@@ -346,7 +367,7 @@ abstract class base implements \IteratorAggregate {
     /**
      * Return auxiliary data that was stored in logs.
      *
-     * TODO: MDL-37658
+     * TODO MDL-41331: Properly define this method once logging is finalised.
      *
      * @return array the format is standardised by logging API
      */
@@ -395,50 +416,51 @@ abstract class base implements \IteratorAggregate {
      * @throws \coding_exception
      */
     protected final function validate_before_trigger() {
-        global $DB;
+        global $DB, $CFG;
 
         if (empty($this->data['crud'])) {
             throw new \coding_exception('crud must be specified in init() method of each method');
         }
-        if (empty($this->data['level'])) {
+        if (!isset($this->data['level'])) {
             throw new \coding_exception('level must be specified in init() method of each method');
         }
         if (!empty($this->data['objectid']) and empty($this->data['objecttable'])) {
             throw new \coding_exception('objecttable must be specified in init() method if objectid present');
         }
 
-        if (debugging('', DEBUG_DEVELOPER)) { // This should be replaced by new $CFG->slowdebug flag if introduced.
+        if ($CFG->debugdeveloper) {
             // Ideally these should be coding exceptions, but we need to skip these for performance reasons
             // on production servers.
 
             if (!in_array($this->data['crud'], array('c', 'r', 'u', 'd'), true)) {
-                debugging("Invalid event crud value specified.");
+                debugging("Invalid event crud value specified.", DEBUG_DEVELOPER);
             }
-            if (!is_number($this->data['level'])) {
-                debugging('Event property level must be a number');
+            if (!in_array($this->data['level'], array(self::LEVEL_OTHER, self::LEVEL_TEACHING, self::LEVEL_PARTICIPATING))) {
+                // Bitwise combination of levels is not allowed at this stage.
+                debugging('Event property level must a constant value, see event_base::LEVEL_*', DEBUG_DEVELOPER);
             }
             if (self::$fields !== array_keys($this->data)) {
-                debugging('Number of event data fields must not be changed in event classes');
+                debugging('Number of event data fields must not be changed in event classes', DEBUG_DEVELOPER);
             }
             $encoded = json_encode($this->data['other']);
             if ($encoded === false or $this->data['other'] !== json_decode($encoded, true)) {
-                debugging('other event data must be compatible with json encoding');
+                debugging('other event data must be compatible with json encoding', DEBUG_DEVELOPER);
             }
             if ($this->data['userid'] and !is_number($this->data['userid'])) {
-                debugging('Event property userid must be a number');
+                debugging('Event property userid must be a number', DEBUG_DEVELOPER);
             }
             if ($this->data['courseid'] and !is_number($this->data['courseid'])) {
-                debugging('Event property courseid must be a number');
+                debugging('Event property courseid must be a number', DEBUG_DEVELOPER);
             }
             if ($this->data['objectid'] and !is_number($this->data['objectid'])) {
-                debugging('Event property objectid must be a number');
+                debugging('Event property objectid must be a number', DEBUG_DEVELOPER);
             }
             if ($this->data['relateduserid'] and !is_number($this->data['relateduserid'])) {
-                debugging('Event property relateduserid must be a number');
+                debugging('Event property relateduserid must be a number', DEBUG_DEVELOPER);
             }
             if ($this->data['objecttable']) {
                 if (!$DB->get_manager()->table_exists($this->data['objecttable'])) {
-                    debugging('Unknown table specified in objecttable field');
+                    debugging('Unknown table specified in objecttable field', DEBUG_DEVELOPER);
                 }
             }
         }
@@ -521,7 +543,7 @@ abstract class base implements \IteratorAggregate {
      * @throws \coding_exception if used after ::trigger()
      */
     public final function add_record_snapshot($tablename, $record) {
-        global $DB;
+        global $DB, $CFG;
 
         if ($this->triggered) {
             throw new \coding_exception('It is not possible to add snapshots after triggering of events');
@@ -529,9 +551,9 @@ abstract class base implements \IteratorAggregate {
 
         // NOTE: this might use some kind of MUC cache,
         //       hopefully we will not run out of memory here...
-        if (debugging('', DEBUG_DEVELOPER)) { // This should be replaced by new $CFG->slowdebug flag if introduced.
+        if ($CFG->debugdeveloper) {
             if (!$DB->get_manager()->table_exists($tablename)) {
-                debugging("Invalid table name '$tablename' specified, database table does not exist.");
+                debugging("Invalid table name '$tablename' specified, database table does not exist.", DEBUG_DEVELOPER);
             }
         }
         $this->recordsnapshots[$tablename][$record->id] = $record;
index 9c2718b..8f68647 100644 (file)
@@ -45,8 +45,7 @@ class blog_entry_created extends \core\event\base {
         $this->context = \context_system::instance();
         $this->data['objecttable'] = 'post';
         $this->data['crud'] = 'c';
-        // TODO: MDL-37658 set level.
-        $this->data['level'] = 50;
+        $this->data['level'] = self::LEVEL_PARTICIPATING;
     }
 
     /**
index 9de612d..9d02aab 100644 (file)
@@ -44,8 +44,7 @@ class blog_entry_deleted extends \core\event\base {
         $this->context = \context_system::instance();
         $this->data['objecttable'] = 'post';
         $this->data['crud'] = 'd';
-        // TODO: MDL-37658 set level.
-        $this->data['level'] = 50;
+        $this->data['level'] = self::LEVEL_PARTICIPATING;
     }
 
     /**
diff --git a/lib/classes/event/cohort_created.php b/lib/classes/event/cohort_created.php
new file mode 100644 (file)
index 0000000..0db4d7b
--- /dev/null
@@ -0,0 +1,92 @@
+<?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/>.
+
+/**
+ * Cohort updated event.
+ *
+ * @package    core
+ * @copyright  2013 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cohort created event class.
+ *
+ * @package    core
+ * @copyright  2013 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cohort_created extends base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'c';
+        $this->data['level'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'cohort';
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('event_cohort_created', 'core_cohort');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return 'Cohort '.$this->objectid.' was created by '.$this->userid.' at context '.$this->contextid;
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/cohort/index.php', array('contextid' => $this->contextid));
+    }
+
+    /**
+     * Return legacy event name.
+     *
+     * @return string legacy event name
+     */
+    public static function get_legacy_eventname() {
+        return 'cohort_added';
+    }
+
+    /**
+     * Return legacy event data.
+     *
+     * @return stdClass
+     */
+    protected function get_legacy_eventdata() {
+        return $this->get_record_snapshot('cohort', $this->objectid);
+    }
+}
diff --git a/lib/classes/event/cohort_deleted.php b/lib/classes/event/cohort_deleted.php
new file mode 100644 (file)
index 0000000..b43b110
--- /dev/null
@@ -0,0 +1,92 @@
+<?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/>.
+
+/**
+ * Cohort deleted event.
+ *
+ * @package    core
+ * @copyright  2013 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cohort deleted event class.
+ *
+ * @package    core
+ * @copyright  2013 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cohort_deleted extends base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'd';
+        $this->data['level'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'cohort';
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('event_core_deleted', 'core_cohort');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return 'Cohort '.$this->objectid.' was deleted by '.$this->userid.' from context '.$this->contextid;
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/cohort/index.php', array('contextid' => $this->contextid));
+    }
+
+    /**
+     * Return legacy event name.
+     *
+     * @return null|string legacy event name
+     */
+    public static function get_legacy_eventname() {
+        return 'cohort_deleted';
+    }
+
+    /**
+     * Return legacy event data.
+     *
+     * @return stdClass
+     */
+    protected function get_legacy_eventdata() {
+        return $this->get_record_snapshot('cohort', $this->objectid);
+    }
+}
diff --git a/lib/classes/event/cohort_member_added.php b/lib/classes/event/cohort_member_added.php
new file mode 100644 (file)
index 0000000..a7e52eb
--- /dev/null
@@ -0,0 +1,95 @@
+<?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/>.
+
+/**
+ * User added to a cohort event.
+ *
+ * @package    core
+ * @copyright  2013 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * User added to a cohort event class.
+ *
+ * @package    core
+ * @copyright  2013 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cohort_member_added extends base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'c';
+        $this->data['level'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'cohort';
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('event_cohort_member_added', 'core_cohort');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return 'User '.$this->relateduserid.' was added to cohort '.$this->objectid.' by user '.$this->userid;
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/cohort/assign.php', array('id' => $this->objectid));
+    }
+
+    /**
+     * Return legacy event name.
+     *
+     * @return string legacy event name.
+     */
+    public static function get_legacy_eventname() {
+        return 'cohort_member_added';
+    }
+
+    /**
+     * Return legacy event data.
+     *
+     * @return stdClass
+     */
+    protected function get_legacy_eventdata() {
+        $data = new \stdClass();
+        $data->cohortid = $this->objectid;
+        $data->userid = $this->relateduserid;
+        return $data;
+    }
+}
diff --git a/lib/classes/event/cohort_member_removed.php b/lib/classes/event/cohort_member_removed.php
new file mode 100644 (file)
index 0000000..b3f47bd
--- /dev/null
@@ -0,0 +1,96 @@
+<?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/>.
+
+/**
+ * User removed from a cohort event.
+ *
+ * @package    core
+ * @copyright  2013 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * User removed from a cohort event class.
+ *
+ * @package    core
+ * @copyright  2013 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+class cohort_member_removed extends base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'd';
+        $this->data['level'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'cohort';
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('event_cohort_member_removed', 'core_cohort');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return 'User '.$this->relateduserid.' was removed from cohort '.$this->objectid.' by user '.$this->userid;
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/cohort/assign.php', array('id' => $this->objectid));
+    }
+
+    /**
+     * Return legacy event name.
+     *
+     * @return string legacy event name.
+     */
+    public static function get_legacy_eventname() {
+        return 'cohort_member_removed';
+    }
+
+    /**
+     * Return legacy event data.
+     *
+     * @return stdClass
+     */
+    protected function get_legacy_eventdata() {
+        $data = new \stdClass();
+        $data->cohortid = $this->objectid;
+        $data->userid = $this->relateduserid;
+        return $data;
+    }
+}
diff --git a/lib/classes/event/cohort_updated.php b/lib/classes/event/cohort_updated.php
new file mode 100644 (file)
index 0000000..f9b6a07
--- /dev/null
@@ -0,0 +1,92 @@
+<?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/>.
+
+/**
+ * Cohort updated event.
+ *
+ * @package    core
+ * @copyright  2013 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cohort updated event class.
+ *
+ * @package    core
+ * @copyright  2013 Dan Poltawski <dan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cohort_updated extends base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'u';
+        $this->data['level'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'cohort';
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('event_cohort_updated', 'core_cohort');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return 'Cohort '.$this->objectid.' was updated by '.$this->userid.' at context '.$this->contextid;
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/cohort/edit.php', array('id' => $this->objectid));
+    }
+
+    /**
+     * Return legacy event name.
+     *
+     * @return string legacy event name.
+     */
+    public static function get_legacy_eventname() {
+        return 'cohort_updated';
+    }
+
+    /**
+     * Return legacy event data.
+     *
+     * @return stdClass
+     */
+    protected function get_legacy_eventdata() {
+        return $this->get_record_snapshot('cohort', $this->objectid);
+    }
+}
diff --git a/lib/classes/event/course_category_deleted.php b/lib/classes/event/course_category_deleted.php
new file mode 100644 (file)
index 0000000..8f0b4d3
--- /dev/null
@@ -0,0 +1,95 @@
+<?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/>.
+
+namespace core\event;
+
+/**
+ * category deleted event.
+ *
+ * @package    core
+ * @copyright  2013 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class course_category_deleted extends base {
+
+    /**
+     * The course category class used for legacy reasons.
+     */
+    private $coursecat;
+
+    /**
+     * Initialise the event data.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'course_categories';
+        $this->data['crud'] = 'd';
+        $this->data['level'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventcoursecategorydeleted');
+    }
+
+    /**
+     * Returns non-localised description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "Category {$this->objectid} was deleted by user {$this->userid}";
+    }
+
+    /**
+     * Returns the name of the legacy event.
+     *
+     * @return string legacy event name
+     */
+    public static function get_legacy_eventname() {
+        return 'course_category_deleted';
+    }
+
+    /**
+     * Returns the legacy event data.
+     *
+     * @return coursecat the category that was deleted
+     */
+    protected function get_legacy_eventdata() {
+        return $this->coursecat;
+    }
+
+    /**
+     * Set the legacy event data.
+     *
+     * @param coursecat $class instance of the coursecat class
+     */
+    public function set_legacy_eventdata($class) {
+        $this->coursecat = $class;
+    }
+
+    /**
+     * Return legacy data for add_to_log().
+     *
+     * @return array
+     */
+    protected function get_legacy_logdata() {
+        return array(SITEID, 'category', 'delete', 'index.php', $this->other['name'] . '(ID ' . $this->objectid . ')');
+    }
+}
index e57ddae..3c7389a 100644 (file)
@@ -31,8 +31,7 @@ class course_completed extends base {
     protected function init() {
         $this->data['objecttable'] = 'course_completions';
         $this->data['crud'] = 'u';
-        // TODO: MDL-37658 set level.
-        $this->data['level'] = 50;
+        $this->data['level'] = self::LEVEL_PARTICIPATING;
     }
 
     /**
diff --git a/lib/classes/event/course_completion_updated.php b/lib/classes/event/course_completion_updated.php
new file mode 100644 (file)
index 0000000..bd1dcbd
--- /dev/null
@@ -0,0 +1,81 @@
+<?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/>.
+
+/**
+ * Event when course module completion is updated.
+ *
+ * @package    core
+ * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event when course module completion is updated.
+ *
+ * @package    core
+ * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class course_completion_updated extends base {
+
+    /**
+     * Initialise required event data properties.
+     */
+    protected function init() {
+        $this->data['crud'] = 'u';
+        $this->data['level'] = self::LEVEL_PARTICIPATING;
+    }
+
+    /**
+     * Returns localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return new get_string('eventcoursecompletionupdated', 'core_completion');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return 'Course completion for course' . $this->courseid . ' is updated by user ' . $this->userid;
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new moodle_url('/course/completion.php', array('id' => $this->courseid));
+    }
+
+    /**
+     * Return legacy add_to_log() data.
+     *
+     * @return array of parameters to be passed to legacy add_to_log() function.
+     */
+    protected function get_legacy_logdata() {
+        return array($this->courseid, 'course', 'completion updated', 'completion.php?id=' . $this->courseid);
+    }
+}
diff --git a/lib/classes/event/course_content_deleted.php b/lib/classes/event/course_content_deleted.php
new file mode 100644 (file)
index 0000000..9cb3309
--- /dev/null
@@ -0,0 +1,76 @@
+<?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/>.
+
+namespace core\event;
+
+/**
+ * Course content_deleted event.
+ *
+ * @package    core
+ * @copyright  2013 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class course_content_deleted extends base {
+
+    /**
+     * Initialise the event data.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'course';
+        $this->data['crud'] = 'd';
+        $this->data['level'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventcoursecontentdeleted');
+    }
+
+    /**
+     * Returns non-localised description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "Course content was deleted by user {$this->userid}";
+    }
+
+    /**
+     * Returns the name of the legacy event.
+     *
+     * @return string legacy event name
+     */
+    public static function get_legacy_eventname() {
+        return 'course_content_removed';
+    }
+
+    /**
+     * Returns the legacy event data.
+     *
+     * @return \stdClass the course the content was deleted from
+     */
+    protected function get_legacy_eventdata() {
+        $course = $this->get_record_snapshot('course', $this->objectid);
+        $course->context = $this->context;
+        $course->options = $this->other['options'];
+
+        return $course;
+    }
+}
diff --git a/lib/classes/event/course_created.php b/lib/classes/event/course_created.php
new file mode 100644 (file)
index 0000000..040f830
--- /dev/null
@@ -0,0 +1,90 @@
+<?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/>.
+
+namespace core\event;
+
+/**
+ * Course created event.
+ *
+ * @package    core
+ * @copyright  2013 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class course_created extends base {
+
+    /**
+     * Initialise the event data.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'course';
+        $this->data['crud'] = 'c';
+        $this->data['level'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventcoursecreated');
+    }
+
+    /**
+     * Returns non-localised description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "Course {$this->objectid} was created by user {$this->userid}";
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/course/view.php', array('id' => $this->objectid));
+    }
+
+    /**
+     * Returns the name of the legacy event.
+     *
+     * @return string legacy event name
+     */
+    public static function get_legacy_eventname() {
+        return 'course_created';
+    }
+
+    /**
+     * Returns the legacy event data.
+     *
+     * @return \stdClass the course that was created
+     */
+    protected function get_legacy_eventdata() {
+        return $this->get_record_snapshot('course', $this->objectid);
+    }
+
+    /**
+     * Return legacy data for add_to_log().
+     *
+     * @return array
+     */
+    protected function get_legacy_logdata() {
+        return array(SITEID, 'course', 'new', 'view.php?id=' . $this->objectid, $this->other['fullname'] . ' (ID ' . $this->objectid . ')');
+    }
+}
diff --git a/lib/classes/event/course_deleted.php b/lib/classes/event/course_deleted.php
new file mode 100644 (file)
index 0000000..2cf551f
--- /dev/null
@@ -0,0 +1,84 @@
+<?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/>.
+
+namespace core\event;
+
+/**
+ * Course deleted event.
+ *
+ * @package    core
+ * @copyright  2013 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class course_deleted extends base {
+
+    /**
+     * Initialise the event data.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'course';
+        $this->data['crud'] = 'd';
+        $this->data['level'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventcoursedeleted');
+    }
+
+    /**
+     * Returns non-localised description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "Course {$this->courseid} was deleted by user {$this->userid}";
+    }
+
+    /**
+     * Returns the name of the legacy event.
+     *
+     * @return string legacy event name
+     */
+    public static function get_legacy_eventname() {
+        return 'course_deleted';
+    }
+
+    /**
+     * Returns the legacy event data.
+     *
+     * @return \stdClass the course that was deleted
+     */
+    protected function get_legacy_eventdata() {
+        $course = $this->get_record_snapshot('course', $this->objectid);
+        $course->context = $this->context;
+
+        return $course;
+    }
+
+    /**
+     * Return legacy data for add_to_log().
+     *
+     * @return array
+     */
+    protected function get_legacy_logdata() {
+        return array(SITEID, 'course', 'delete', 'view.php?id=' . $this->objectid, $this->other['fullname']  . '(ID ' . $this->objectid . ')');
+    }
+}
index c57f0c7..fbc8c7b 100644 (file)
@@ -31,8 +31,7 @@ class course_module_completion_updated extends base {
     protected function init() {
         $this->data['objecttable'] = 'course_modules_completion';
         $this->data['crud'] = 'u';
-        // TODO: MDL-37658 set level.
-        $this->data['level'] = 50;
+        $this->data['level'] = self::LEVEL_PARTICIPATING;
     }
 
     /**
diff --git a/lib/classes/event/course_restored.php b/lib/classes/event/course_restored.php
new file mode 100644 (file)
index 0000000..f684463
--- /dev/null
@@ -0,0 +1,89 @@
+<?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/>.
+
+namespace core\event;
+
+/**
+ * Course restored event.
+ *
+ * @package    core
+ * @copyright  2013 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class course_restored extends base {
+
+    /**
+     * Initialise the event data.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'course';
+        $this->data['crud'] = 'c';
+        $this->data['level'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventcourserestored');
+    }
+
+    /**
+     * Returns non-localised description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "Course {$this->objectid} was restored by user {$this->userid}";
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/course/view.php', array('id' => $this->objectid));
+    }
+
+    /**
+     * Returns the name of the legacy event.
+     *
+     * @return string legacy event name
+     */
+    public static function get_legacy_eventname() {
+        return 'course_restored';
+    }
+
+    /**
+     * Returns the legacy event data.
+     *
+     * @return \stdClass the legacy event data
+     */
+    protected function get_legacy_eventdata() {
+        return (object) array(
+            'courseid' => $this->objectid,
+            'userid' => $this->userid,
+            'type' => $this->other['type'],
+            'target' => $this->other['target'],
+            'mode' => $this->other['mode'],
+            'operation' => $this->other['operation'],
+            'samesite' => $this->other['samesite'],
+        );
+    }
+}
diff --git a/lib/classes/event/course_section_updated.php b/lib/classes/event/course_section_updated.php
new file mode 100644 (file)
index 0000000..5a7d9e0
--- /dev/null
@@ -0,0 +1,85 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Course section updated.
+ *
+ * @package    core
+ * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Course section updated.
+ *
+ * @package    core
+ * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class course_section_updated extends base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'course_sections';
+        $this->data['crud'] = 'u';
+        $this->data['level'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventcoursesectionupdated');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return 'Course ' . $this->courseid . ' section ' . $this->other['sectionnum'] . ' updated by user ' . $this->userid;
+    }
+
+    /**
+     * Get URL related to the action.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/course/editsection.php', array('id' => $this->objectid));
+    }
+
+    /**
+     * Return legacy data for add_to_log().
+     *
+     * @return array
+     */
+    protected function get_legacy_logdata() {
+        $sectiondata = $this->get_record_snapshot('course_sections', $this->objectid);
+        return array($this->courseid, 'course', 'editsection', 'editsection.php?id=' . $this->objectid, $sectiondata->section);
+    }
+}
diff --git a/lib/classes/event/course_updated.php b/lib/classes/event/course_updated.php
new file mode 100644 (file)
index 0000000..b76dd65
--- /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/>.
+
+namespace core\event;
+
+/**
+ * Course updated event.
+ *
+ * @package    core
+ * @copyright  2013 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class course_updated extends base {
+
+    /** @var array The legacy log data. */
+    private $legacylogdata;
+
+    /**
+     * Initialise the event data.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'course';
+        $this->data['crud'] = 'u';
+        $this->data['level'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventcourseupdated');
+    }
+
+    /**
+     * Returns non-localised description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "Course {$this->courseid} was updated by user {$this->userid}";
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/course/view.php', array('id' => $this->objectid));
+    }
+
+    /**
+     * Returns the name of the legacy event.
+     *
+     * @return string legacy event name
+     */
+    public static function get_legacy_eventname() {
+        return 'course_updated';
+    }
+
+    /**
+     * Returns the legacy event data.
+     *
+     * @return \stdClass the course that was updated
+     */
+    protected function get_legacy_eventdata() {
+        return $this->get_record_snapshot('course', $this->objectid);
+    }
+
+    /**
+     * Set the legacy data used for add_to_log().
+     *
+     * @param array $logdata
+     */
+    public function set_legacy_logdata($logdata) {
+        $this->legacylogdata = $logdata;
+    }
+
+    /**
+     * Return legacy data for add_to_log().
+     *
+     * @return array
+     */
+    protected function get_legacy_logdata() {
+        return $this->legacylogdata;
+    }
+}
index 538e56c..ad4ac9a 100644 (file)
@@ -30,8 +30,7 @@ class role_allow_assign_updated extends base {
      */
     protected function init() {
         $this->data['crud'] = 'u';
-        // TODO: MDL-41040 set level.
-        $this->data['level'] = 50;
+        $this->data['level'] = self::LEVEL_OTHER;
     }
 
     /**
index 2edee4b..fc7c5ce 100644 (file)
@@ -30,8 +30,7 @@ class role_allow_override_updated extends base {
      */
     protected function init() {
         $this->data['crud'] = 'u';
-        // TODO: MDL-41040 set level.
-        $this->data['level'] = 50;
+        $this->data['level'] = self::LEVEL_OTHER;
     }
 
     /**
index 04606e7..20e45eb 100644 (file)
@@ -30,8 +30,7 @@ class role_allow_switch_updated extends base {
      */
     protected function init() {
         $this->data['crud'] = 'u';
-        // TODO: MDL-41040 set level.
-        $this->data['level'] = 50;
+        $this->data['level'] = self::LEVEL_OTHER;
     }
 
     /**
index 9e74839..1f763ab 100644 (file)
@@ -28,8 +28,7 @@ class role_assigned extends base {
     protected function init() {
         $this->data['objecttable'] = 'role';
         $this->data['crud'] = 'c';
-        // TODO: MDL-37658 set level
-        $this->data['level'] = 50;
+        $this->data['level'] = self::LEVEL_OTHER;
     }
 
     /**
index 6f836dc..5d1f89c 100644 (file)
@@ -34,8 +34,7 @@ class role_capabilities_updated extends base {
     protected function init() {
         $this->data['objecttable'] = 'role';
         $this->data['crud'] = 'u';
-        // TODO: MDL-41040 set level.
-        $this->data['level'] = 50;
+        $this->data['level'] = self::LEVEL_OTHER;
     }
 
     /**
index 11e15b9..969ebbb 100644 (file)
@@ -31,8 +31,7 @@ class role_deleted extends base {
     protected function init() {
         $this->data['objecttable'] = 'role';
         $this->data['crud'] = 'd';
-        // TODO: MDL-41040 set level.
-        $this->data['level'] = 50;
+        $this->data['level'] = self::LEVEL_OTHER;
     }
 
     /**
index 3bb4a10..4735797 100644 (file)
@@ -28,8 +28,7 @@ class role_unassigned extends base {
     protected function init() {
         $this->data['objecttable'] = 'role';
         $this->data['crud'] = 'd';
-        // TODO: MDL-37658 set level
-        $this->data['level'] = 50;
+        $this->data['level'] = self::LEVEL_OTHER;
     }
 
     /**
index 7cfc3f8..2f842fa 100644 (file)
@@ -89,7 +89,7 @@ class user_loggedin extends \core\event\base {
     protected function init() {
         $this->context = \context_system::instance();
         $this->data['crud'] = 'r';
-        $this->data['level'] = 50;          // TODO MDL-37658.
+        $this->data['level'] = self::LEVEL_OTHER;
         $this->data['objecttable'] = 'user';
     }
 
diff --git a/lib/classes/event/user_loggedinas.php b/lib/classes/event/user_loggedinas.php
new file mode 100644 (file)
index 0000000..50012d8
--- /dev/null
@@ -0,0 +1,85 @@
+<?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/>.
+
+/**
+ * User loggedinas event.
+ *
+ * @package    core
+ * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * User loggedinas event class.
+ *
+ * @package    core
+ * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class user_loggedinas extends base {
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'r';
+        $this->data['level'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'user';
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventuserloggedinas', 'auth');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return 'Userid ' . $this->userid . ' has logged in as '. $this->relateduserid;
+    }
+
+    /**
+     * Return legacy data for add_to_log().
+     *
+     * @return array
+     */
+    protected function get_legacy_logdata() {
+        return array($this->courseid, 'course', 'loginas', '../user/view.php?id=' . $this->courseid . '&amp;user=' . $this->userid,
+            $this->other['originalusername'] . ' -> ' . $this->other['loggedinasusername']);
+    }
+
+    /**
+     * Get URL related to the action.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/user/view.php', array('id' => $this->objectid));
+    }
+}
diff --git a/lib/classes/useragent.php b/lib/classes/useragent.php
new file mode 100644 (file)
index 0000000..e8c1906
--- /dev/null
@@ -0,0 +1,751 @@
+<?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/>.
+
+/**
+ * Environment class to aid with the detection and establishment of the working environment.
+ *
+ * @package    core
+ * @copyright  2013 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * The user agent class.
+ *
+ * It's important to note that we do not like browser sniffing and its use in core code is highly discouraged.
+ * No new uses of this API will be integrated unless there is absolutely no alternative.
+ *
+ * This API supports the few browser checks we do have in core, all of which one day will hopefully be removed.
+ * The API will remain to support any third party use out there, however at some point like all code it will be deprecated.
+ *
+ * Use sparingly and only with good cause!
+ *
+ * @package    core
+ * @copyright  2013 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_useragent {
+
+    /**
+     * The default for devices, think of as a computer.
+     */
+    const DEVICETYPE_DEFAULT = 'default';
+    /**
+     * Legacy devices, or at least legacy browsers. These are older devices/browsers
+     * that don't support standards.
+     */
+    const DEVICETYPE_LEGACY = 'legacy';
+    /**
+     * Mobile devices like your cell phone or hand held gaming device.
+     */
+    const DEVICETYPE_MOBILE = 'mobile';
+    /**
+     * Tables, larger than hand held, but still easily portable and smaller than a laptop.
+     */
+    const DEVICETYPE_TABLET = 'tablet';
+
+    /**
+     * An instance of this class.
+     * @var core_useragent
+     */
+    protected static $instance = null;
+
+    /**
+     * The device types we track.
+     * @var array
+     */
+    public static $devicetypes = array(
+        self::DEVICETYPE_DEFAULT,
+        self::DEVICETYPE_LEGACY,
+        self::DEVICETYPE_MOBILE,
+        self::DEVICETYPE_TABLET
+    );
+
+    /**
+     * The current requests user agent string if there was one.
+     * @var string|bool|null Null until initialised, false if none available, or string when available.
+     */
+    protected $useragent = null;
+
+    /**
+     * The users device type, one of self::DEVICETYPE_*.
+     * @var string null until initialised
+     */
+    protected $devicetype = null;
+
+    /**
+     * Custom device types entered into the admin interface.
+     * @var array
+     */
+    protected $devicetypecustoms = array();
+
+    /**
+     * True if the user agent supports the display of svg images. False if not.
+     * @var bool|null Null until initialised, then true or false.
+     */
+    protected $supportssvg = null;
+
+    /**
+     * Get an instance of the user agent object.
+     *
+     * @param bool $reload If set to true the user agent will be reset and all ascertations remade.
+     * @param string $forceuseragent The string to force as the user agent, don't use unless absolutely unavoidable.
+     * @return core_useragent
+     */
+    public static function instance($reload = false, $forceuseragent = null) {
+        if (!self::$instance || $reload) {
+            self::$instance = new core_useragent($forceuseragent);
+        }
+        return self::$instance;
+    }
+
+    /**
+     * Constructs a new user agent object. Publically you must use the instance method above.
+     *
+     * @param string|null $forceuseragent Optional a user agent to force.
+     */
+    protected function __construct($forceuseragent = null) {
+        global $CFG;
+        if (!empty($CFG->devicedetectregex)) {
+            $this->devicetypecustoms = json_decode($CFG->devicedetectregex);
+        }
+        if ($forceuseragent !== null) {
+            $this->useragent = $forceuseragent;
+        } else if (!empty($_SERVER['HTTP_USER_AGENT'])) {
+            $this->useragent = $_SERVER['HTTP_USER_AGENT'];
+        } else {
+            $this->useragent = false;
+            $this->devicetype = self::DEVICETYPE_DEFAULT;
+        }
+    }
+
+    /**
+     * Returns the user agent string.
+     * @return bool|string The user agent string or false if one isn't available.
+     */
+    public static function get_user_agent_string() {
+        $instance = self::instance();
+        return $instance->useragent;
+    }
+
+    /**
+     * Returns the device type we believe is being used.
+     * @return string
+     */
+    public static function get_device_type() {
+        $instance = self::instance();
+        if ($instance->devicetype === null) {
+            return $instance->guess_device_type();
+        }
+        return $instance->devicetype;
+    }
+
+    /**
+     * Guesses the device type the user agent is running on.
+     *
+     * @return string
+     */
+    protected function guess_device_type() {
+        global $CFG;
+        if (empty($CFG->enabledevicedetection)) {
+            $this->devicetype = self::DEVICETYPE_DEFAULT;
+            return $this->devicetype;
+        }
+        foreach ($this->devicetypecustoms as $value => $regex) {
+            if (preg_match($regex, $this->useragent)) {
+                $this->devicetype = $value;
+                return $this->devicetype;
+            }
+        }
+        if ($this->is_useragent_mobile()) {
+            $this->devicetype = 'mobile';
+        } else if ($this->is_useragent_tablet()) {
+            $this->devicetype = 'tablet';
+        } else if (substr($this->useragent, 0, 34) === 'Mozilla/4.0 (compatible; MSIE 6.0;') {
+            // Safe way to check for IE6 and not get false positives for some IE 7/8 users.
+            $this->devicetype = 'legacy';
+        } else {
+            $this->devicetype = self::DEVICETYPE_DEFAULT;
+        }
+        return $this->devicetype;
+    }
+
+    /**
+     * Returns true if the user appears to be on a mobile device.
+     * @return bool
+     */
+    protected function is_useragent_mobile() {
+        // Mobile detection PHP direct copy from open source detectmobilebrowser.com.
+        $phonesregex = '/android .+ mobile|avantgo|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i';
+        $modelsregex = '/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|e\-|e\/|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(di|rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|xda(\-|2|g)|yas\-|your|zeto|zte\-/i';
+        return (preg_match($phonesregex, $this->useragent) || preg_match($modelsregex, substr($this->useragent, 0, 4)));
+    }
+
+    /**
+     * Returns true if the user appears to be on a tablet.
+     * @return int
+     */
+    protected function is_useragent_tablet() {
+        $tabletregex = '/Tablet browser|android|iPad|iProd|GT-P1000|GT-I9000|SHW-M180S|SGH-T849|SCH-I800|Build\/ERE27|sholest/i';
+        return (preg_match($tabletregex, $this->useragent));
+    }
+
+    /**
+     * Gets a list of known device types.
+     *
+     * @param bool $includecustomtypes If set to true we'll include types that have been added by the admin.
+     * @return array
+     */
+    public static function get_device_type_list($includecustomtypes = true) {
+        $types = self::$devicetypes;
+        if ($includecustomtypes) {
+            $instance = self::instance();
+            $types = array_merge($types, array_keys($instance->devicetypecustoms));
+        }
+        return $types;
+    }
+
+    /**
+     * Returns the theme to use for the given device type.
+     *
+     * This used to be get_selected_theme_for_device_type.
+     * @param null|string $devicetype The device type to find out for. Defaults to the device the user is using,
+     * @return bool
+     */
+    public static function get_device_type_theme($devicetype = null) {
+        global $CFG;
+        if ($devicetype === null) {
+            $devicetype = self::get_device_type();
+        }
+        $themevarname = self::get_device_type_cfg_var_name($devicetype);
+        if (empty($CFG->$themevarname)) {
+            return false;
+        }
+        return $CFG->$themevarname;
+    }
+
+    /**
+     * Returns the CFG var used to find the theme to use for the given device.
+     *
+     * Used to be get_device_cfg_var_name.
+     *
+     * @param null|string $devicetype The device type to find out for. Defaults to the device the user is using,
+     * @return string
+     */
+    public static function get_device_type_cfg_var_name($devicetype = null) {
+        if ($devicetype == self::DEVICETYPE_DEFAULT || empty($devicetype)) {
+            return 'theme';
+        }
+        return 'theme' . $devicetype;
+    }
+
+    /**
+     * Gets the device type the user is currently using.
+     * @return string
+     */
+    public static function get_user_device_type() {
+        $device = self::get_device_type();
+        $switched = get_user_preferences('switchdevice'.$device, false);
+        if ($switched != false) {
+            return $switched;
+        }
+        return $device;
+    }
+
+    /**
+     * Switches the device type we think the user is using to what ever was given.
+     * @param string $newdevice
+     * @return bool
+     * @throws coding_exception
+     */
+    public static function set_user_device_type($newdevice) {
+        $devicetype = self::get_device_type();
+        if ($newdevice == $devicetype) {
+            unset_user_preference('switchdevice'.$devicetype);
+            return true;
+        } else {
+            $devicetypes = self::get_device_type_list();
+            if (in_array($newdevice, $devicetypes)) {
+                set_user_preference('switchdevice'.$devicetype, $newdevice);
+                return true;
+            }
+        }
+        throw new coding_exception('Invalid device type provided to set_user_device_type');
+    }
+
+    /**
+     * Returns true if the user agent matches the given brand and the version is equal to or greater than that specified.
+     *
+     * @param string $brand The branch to check for.
+     * @param scalar $version The version if we need to find out if it is equal to or greater than that specified.
+     * @return bool
+     */
+    public static function check_browser_version($brand, $version = null) {
+        switch ($brand) {
+
+            case 'MSIE':
+                // Internet Explorer.
+                return self::check_ie_version($version);
+
+            case 'Firefox':
+                // Mozilla Firefox browsers.
+                return self::check_firefox_version($version);
+
+            case 'Chrome':
+                return self::check_chrome_version($version);
+
+            case 'Opera':
+                // Opera.
+                return self::check_opera_version($version);
+
+            case 'Safari':
+                // Desktop version of Apple Safari browser - no mobile or touch devices.
+                return self::check_safari_version($version);
+
+            case 'Safari iOS':
+                // Safari on iPhone, iPad and iPod touch.
+                return self::check_safari_ios_version($version);
+
+            case 'WebKit':
+                // WebKit based browser - everything derived from it (Safari, Chrome, iOS, Android and other mobiles).
+                return self::check_webkit_version($version);
+
+            case 'Gecko':
+                // Gecko based browsers.
+                return self::check_gecko_version($version);
+
+            case 'WebKit Android':
+                // WebKit browser on Android.
+                return self::check_webkit_android_version($version);
+
+            case 'Camino':
+                // OSX browser using Gecke engine.
+                return self::check_camino_version($version);
+        }
+        // Who knows?! doesn't pass anyway.
+        return false;
+    }
+
+    /**
+     * Checks the user agent is camino based and that the version is equal to or greater than that specified.
+     *
+     * Camino browser is at the end of its life, its no longer being developed or supported, just don't worry about it.
+     *
+     * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
+     * @return bool
+     */
+    protected static function check_camino_version($version = null) {
+        // OSX browser using Gecko engine.
+        $useragent = self::get_user_agent_string();
+        if ($useragent === false) {
+            return false;
+        }
+        if (strpos($useragent, 'Camino') === false) {
+            return false;
+        }
+        if (empty($version)) {
+            return true; // No version specified.
+        }
+        if (preg_match("/Camino\/([0-9\.]+)/i", $useragent, $match)) {
+            if (version_compare($match[1], $version) >= 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Checks the user agent is Firefox based and that the version is equal to or greater than that specified.
+     *
+     * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
+     * @return bool
+     */
+    public static function check_firefox_version($version = null) {
+        // Mozilla Firefox browsers.
+        $useragent = self::get_user_agent_string();
+        if ($useragent === false) {
+            return false;
+        }
+        if (strpos($useragent, 'Firefox') === false && strpos($useragent, 'Iceweasel') === false) {
+            return false;
+        }
+        if (empty($version)) {
+            return true; // No version specified..
+        }
+        if (preg_match("/(Iceweasel|Firefox)\/([0-9\.]+)/i", $useragent, $match)) {
+            if (version_compare($match[2], $version) >= 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Checks the user agent is Gecko based and that the version is equal to or greater than that specified.
+     *
+     * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
+     * @return bool
+     */
+    public static function check_gecko_version($version = null) {
+        // Gecko based browsers.
+        // Do not look for dates any more, we expect real Firefox version here.
+        $useragent = self::get_user_agent_string();
+        if ($useragent === false) {
+            return false;
+        }
+        if (empty($version)) {
+            $version = 1;
+        } else if ($version > 20000000) {
+            // This is just a guess, it is not supposed to be 100% accurate!
+            if (preg_match('/^201/', $version)) {
+                $version = 3.6;
+            } else if (preg_match('/^200[7-9]/', $version)) {
+                $version = 3;
+            } else if (preg_match('/^2006/', $version)) {
+                $version = 2;
+            } else {
+                $version = 1.5;
+            }
+        }
+        if (preg_match("/(Iceweasel|Firefox)\/([0-9\.]+)/i", $useragent, $match)) {
+            // Use real Firefox version if specified in user agent string.
+            if (version_compare($match[2], $version) >= 0) {
+                return true;
+            }
+        } else if (preg_match("/Gecko\/([0-9\.]+)/i", $useragent, $match)) {
+            // Gecko might contain date or Firefox revision, let's just guess the Firefox version from the date.
+            $browserver = $match[1];
+            if ($browserver > 20000000) {
+                // This is just a guess, it is not supposed to be 100% accurate!
+                if (preg_match('/^201/', $browserver)) {
+                    $browserver = 3.6;
+                } else if (preg_match('/^200[7-9]/', $browserver)) {
+                    $browserver = 3;
+                } else if (preg_match('/^2006/', $version)) {
+                    $browserver = 2;
+                } else {
+                    $browserver = 1.5;
+                }
+            }
+            if (version_compare($browserver, $version) >= 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Checks the user agent is IE and that the version is equal to or greater than that specified.
+     *
+     * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
+     * @return bool
+     */
+    public static function check_ie_version($version = null) {
+        // Internet Explorer.
+        $useragent = self::get_user_agent_string();
+        if ($useragent === false) {
+            return false;
+        }
+        if (strpos($useragent, 'Opera') !== false) {
+            // Reject Opera.
+            return false;
+        }
+        // In case of IE we have to deal with BC of the version parameter.
+        if (is_null($version)) {
+            $version = 5.5; // Anything older is not considered a browser at all!
+        }
+        // IE uses simple versions, let's cast it to float to simplify the logic here.
+        $version = round($version, 1);
+        // See: http://www.useragentstring.com/pages/Internet%20Explorer/.
+        if (preg_match("/MSIE ([0-9\.]+)/", $useragent, $match)) {
+            $browser = $match[1];
+        } else {
+            return false;
+        }
+        // IE8 and later versions may pretend to be IE7 for intranet sites, use Trident version instead,
+        // the Trident should always describe the capabilities of IE in any emulation mode.
+        if ($browser === '7.0' and preg_match("/Trident\/([0-9\.]+)/", $useragent, $match)) {
+            $browser = $match[1] + 4; // NOTE: Hopefully this will work also for future IE versions.
+        }
+        $browser = round($browser, 1);
+        return ($browser >= $version);
+    }
+
+    /**
+     * Checks the user agent is Opera and that the version is equal to or greater than that specified.
+     *
+     * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
+     * @return bool
+     */
+    public static function check_opera_version($version = null) {
+        // Opera.
+        $useragent = self::get_user_agent_string();
+        if ($useragent === false) {
+            return false;
+        }
+        if (strpos($useragent, 'Opera') === false) {
+            return false;
+        }
+        if (empty($version)) {
+            return true; // No version specified.
+        }
+        // Recent Opera useragents have Version/ with the actual version, e.g.:
+        // Opera/9.80 (Windows NT 6.1; WOW64; U; en) Presto/2.10.289 Version/12.01
+        // That's Opera 12.01, not 9.8.
+        if (preg_match("/Version\/([0-9\.]+)/i", $useragent, $match)) {
+            if (version_compare($match[1], $version) >= 0) {
+                return true;
+            }
+        } else if (preg_match("/Opera\/([0-9\.]+)/i", $useragent, $match)) {
+            if (version_compare($match[1], $version) >= 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Checks the user agent is Webkit based and that the version is equal to or greater than that specified.
+     *
+     * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
+     * @return bool
+     */
+    public static function check_webkit_version($version = null) {
+        // WebKit based browser - everything derived from it (Safari, Chrome, iOS, Android and other mobiles).
+        $useragent = self::get_user_agent_string();
+        if ($useragent === false) {
+            return false;
+        }
+        if (strpos($useragent, 'AppleWebKit') === false) {
+            return false;
+        }
+        if (empty($version)) {
+            return true; // No version specified.
+        }
+        if (preg_match("/AppleWebKit\/([0-9.]+)/i", $useragent, $match)) {
+            if (version_compare($match[1], $version) >= 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Checks the user agent is Safari based and that the version is equal to or greater than that specified.
+     *
+     * @param string|int $version A version to check for, returns true if its equal to or greater than that specified.
+     * @return bool
+     */
+    public static function check_safari_version($version = null) {
+        // Desktop version of Apple Safari browser - no mobile or touch devices.
+        $useragent = self::get_user_agent_string();
+        if ($useragent === false) {
+            return false;
+        }
+        if (strpos($useragent, 'AppleWebKit') === false) {
+            return false;
+        }
+        // Look for AppleWebKit, excluding strings with OmniWeb, Shiira and SymbianOS and any other mobile devices.
+        if (strpos($useragent, 'OmniWeb')) {
+            // Reject OmniWeb.
+            return false;
+        }
+        if (strpos($useragent, 'Shiira')) {
+            // Reject Shiira.
+            return false;
+        }
+        if (strpos($useragent, 'SymbianOS')) {
+            // Reject SymbianOS.
+            return false;
+        }
+        if (strpos($useragent, 'Android')) {
+            // Reject Androids too.
+            return false;
+        }
+        if (strpos($useragent, 'iPhone') or strpos($useragent, 'iPad') or strpos($useragent, 'iPod')) {
+            // No Apple mobile devices here - editor does not work, course ajax is not touch compatible, etc.
+            return false;
+        }
+        if (strpos($useragent, 'Chrome')) { // Reject chrome browsers - it needs to be tested explicitly.
+            return false;
+        }
+
+        if (empty($version)) {
+            return true; // No version specified.
+        }
+        if (preg_match("/AppleWebKit\/([0-9.]+)/i", $useragent, $match)) {
+            if (version_compare($match[1], $version) >= 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Checks the user agent is Chrome based and that the version is equal to or greater than that specified.
+     *
+     * @param string|int $version A version to check for, returns true if its e