Merge branch 'MDL-49237-master' of git://github.com/mastnym/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 2 Mar 2015 20:41:35 +0000 (21:41 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 2 Mar 2015 20:41:35 +0000 (21:41 +0100)
135 files changed:
.gitignore
.jshintrc
Gruntfile.js [new file with mode: 0644]
admin/settings/courses.php
admin/tool/langimport/classes/controller.php
admin/webservice/forms.php
auth/shibboleth/auth.php
auth/shibboleth/config.html
availability/classes/info.php
availability/classes/tree.php
availability/classes/tree_node.php
availability/condition/group/classes/condition.php
availability/condition/grouping/classes/condition.php
availability/upgrade.txt
backup/moodle2/backup_activity_task.class.php
backup/moodle2/backup_course_task.class.php
backup/moodle2/backup_root_task.class.php
backup/moodle2/backup_settingslib.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_root_task.class.php
backup/moodle2/restore_settingslib.php
backup/moodle2/restore_stepslib.php
backup/util/dbops/backup_controller_dbops.class.php
badges/tests/events_test.php [new file with mode: 0644]
blocks/course_overview/lang/en/block_course_overview.php
blocks/tests/behat/configure_block_throughout_site.feature
course/edit.php
course/externallib.php
enrol/renderer.php
enrol/users.php
enrol/users_forms.php
filter/urltolink/filter.php
filter/urltolink/tests/filter_test.php
group/externallib.php
group/tests/externallib_test.php
lang/en/backup.php
lang/en/badges.php
lang/en/enrol.php
lang/en/filters.php
lang/en/form.php
lang/en/moodle.php
lang/en/repository.php
lang/en/role.php
lang/en/webservice.php
lib/accesslib.php
lib/amd/build/first.min.js [new file with mode: 0644]
lib/amd/src/first.js [new file with mode: 0644]
lib/badgeslib.php
lib/classes/event/badge_awarded.php [new file with mode: 0644]
lib/classes/requirejs.php [new file with mode: 0644]
lib/componentlib.class.php
lib/db/install.xml
lib/db/services.php
lib/db/tasks.php
lib/db/upgrade.php
lib/filestorage/zip_archive.php
lib/grade/grade_category.php
lib/javascript-static.js
lib/moodlelib.php
lib/outputrequirementslib.php
lib/requirejs.php [new file with mode: 0644]
lib/requirejs/LICENSE [new file with mode: 0644]
lib/requirejs/jquery-private.js [new file with mode: 0644]
lib/requirejs/moodle-config.js [new file with mode: 0644]
lib/requirejs/require.js [new file with mode: 0644]
lib/requirejs/require.min.js [new file with mode: 0644]
lib/thirdpartylibs.xml
lib/upgrade.txt
lib/weblib.php
login/token.php
message/lib.php
message/tests/behat/manage_contacts.feature
message/tests/messagelib_test.php
mod/assign/backup/moodle2/restore_assign_stepslib.php
mod/assign/lang/en/assign.php
mod/assign/locallib.php
mod/choice/backup/moodle2/backup_choice_stepslib.php
mod/choice/db/install.xml
mod/choice/db/upgrade.php
mod/choice/lang/en/choice.php
mod/choice/lib.php
mod/choice/mod_form.php
mod/choice/renderer.php
mod/choice/tests/behat/allow_preview.feature [new file with mode: 0644]
mod/choice/tests/behat/block_editing.feature [new file with mode: 0644]
mod/choice/tests/behat/my_home.feature [new file with mode: 0644]
mod/choice/upgrade.txt [new file with mode: 0644]
mod/choice/version.php
mod/choice/view.php
mod/forum/lib.php
mod/forum/tests/externallib_test.php
mod/forum/tests/lib_test.php
mod/lesson/classes/event/page_created.php [new file with mode: 0644]
mod/lesson/classes/event/page_deleted.php [new file with mode: 0644]
mod/lesson/classes/event/page_updated.php [new file with mode: 0644]
mod/lesson/lang/en/lesson.php
mod/lesson/locallib.php
mod/lesson/pagetypes/essay.php
mod/lesson/pagetypes/matching.php
mod/lesson/pagetypes/truefalse.php
mod/lesson/tests/events_test.php
mod/lesson/version.php
mod/lti/lang/en/lti.php
mod/scorm/datamodels/scorm_12.js
mod/scorm/datamodels/scormlib.php
mod/workshop/lang/en/workshop.php
package.json [new file with mode: 0644]
portfolio/googledocs/lang/en/portfolio_googledocs.php
portfolio/picasa/lang/en/portfolio_picasa.php
question/type/multianswer/lang/en/qtype_multianswer.php
report/log/classes/table_log.php
repository/googledocs/lang/en/repository_googledocs.php
repository/picasa/lang/en/repository_picasa.php
repository/s3/lang/en/repository_s3.php
tag/user.php
theme/base/style/message.css
theme/bootstrapbase/less/moodle.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/less/moodle/message.less
theme/bootstrapbase/less/moodle/responsive.less
theme/bootstrapbase/style/moodle.css
theme/clean/lang/en/theme_clean.php
theme/more/lang/en/theme_more.php
user/externallib.php
user/filters/checkbox.php
user/filters/cohort.php
user/filters/courserole.php
user/filters/lib.php
user/filters/profilefield.php
user/filters/select.php
user/filters/text.php
user/lib.php
user/tests/behat/filter_idnumber.feature [new file with mode: 0644]
user/tests/externallib_test.php
version.php

index 8b976bb..89ea0d6 100644 (file)
@@ -36,3 +36,4 @@ composer.lock
 # lib/yuilib/version/module/module-coverage.js
 /lib/yuilib/*/*/*-coverage.js
 atlassian-ide-plugin.xml
+/node_modules/
index 9eff324..8b8a806 100644 (file)
--- a/.jshintrc
+++ b/.jshintrc
@@ -34,7 +34,8 @@
     "passfail":     false,
     "plusplus":     false,
     "predef": [
-        "M"
+        "M",
+        "define"
     ],
     "proto":        false,
     "regexdash":    false,
diff --git a/Gruntfile.js b/Gruntfile.js
new file mode 100644 (file)
index 0000000..4c45234
--- /dev/null
@@ -0,0 +1,149 @@
+// 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/>.
+
+/**
+ * @copyright  2014 Andrew Nicols
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Grunt configuration
+ */
+
+module.exports = function(grunt) {
+    var path = require('path'),
+        tasks = {};
+
+    // Project configuration.
+    grunt.initConfig({
+        jshint: {
+            options: {jshintrc: '.jshintrc'},
+            files: ['**/amd/src/*.js']
+        },
+        uglify: {
+            dynamic_mappings: {
+                files: grunt.file.expandMapping(
+                    ['**/src/*.js', '!**/node_modules/**'],
+                    '',
+                    {
+                        cwd: process.env.PWD,
+                        rename: function(destBase, destPath) {
+                            destPath = destPath.replace('src', 'build');
+                            destPath = destPath.replace('.js', '.min.js');
+                            destPath = path.resolve(process.env.PWD, destPath);
+                            return destPath;
+                        }
+                    }
+                )
+            }
+        }
+    });
+
+    tasks.shifter = function() {
+       var  exec = require('child_process').spawn,
+            done = this.async(),
+            args = [],
+            options = {
+                recursive: true,
+                watch: false,
+                walk: false,
+                module: false
+            },
+            shifter;
+
+            // Determine the most appropriate options to run with based upon the current location.
+            if (path.basename(process.env.PWD) === 'src') {
+                // Detect whether we're in a src directory.
+                grunt.log.debug('In a src directory');
+                args.push('--walk');
+                options.walk = true;
+            } else if (path.basename(path.dirname(process.env.PWD)) === 'src') {
+                // Detect whether we're in a module directory.
+                grunt.log.debug('In a module directory');
+                options.module = true;
+            }
+
+            if (grunt.option('watch')) {
+                if (!options.walk && !options.module) {
+                    grunt.fail.fatal('Unable to watch unless in a src or module directory');
+                }
+
+                // It is not advisable to run with recursivity and watch - this
+                // leads to building the build directory in a race-like fashion.
+                grunt.log.debug('Detected a watch - disabling recursivity');
+                options.recursive = false;
+                args.push('--watch');
+            }
+
+            if (options.recursive) {
+                args.push('--recursive');
+            }
+
+            // Always ignore the node_modules directory.
+            args.push('--excludes', 'node_modules');
+
+            // Add the stderr option if appropriate
+            if (grunt.option('verbose')) {
+                args.push('--lint-stderr');
+            }
+
+            // Actually run shifter.
+            shifter = exec(process.cwd() + '/node_modules/shifter/bin/shifter', args, {
+                cwd: process.env.PWD,
+                stdio: 'inherit',
+                env: process.env
+            });
+
+            // Tidy up after exec.
+            shifter.on('exit', function (code) {
+                if (code) {
+                    grunt.fail.fatal('Shifter failed with code: ' + code);
+                } else {
+                    grunt.log.ok('Shifter build complete.');
+                    done();
+                }
+            });
+    };
+
+    tasks.startup = function() {
+        // Are we in a YUI directory?
+        if (path.basename(path.resolve(process.env.PWD, '../../')) == 'yui') {
+            grunt.task.run('shifter');
+        // Are we in an AMD directory?
+        } else if (path.basename(process.env.PWD) == 'amd') {
+            grunt.task.run('jshint');
+            grunt.task.run('uglify');
+        } else {
+            // Run them all!.
+            grunt.task.run('shifter');
+            grunt.task.run('jshint');
+            grunt.task.run('uglify');
+        }
+    };
+
+
+    // Register NPM tasks.
+    grunt.loadNpmTasks('grunt-contrib-uglify');
+    grunt.loadNpmTasks('grunt-contrib-jshint');
+
+    // Register the shifter task.
+    grunt.registerTask('shifter', 'Run Shifter against the current directory', tasks.shifter);
+
+    // Register the startup task.
+    grunt.registerTask('startup', 'Run the correct tasks for the current directory', tasks.startup);
+
+    // Register the default task.
+    grunt.registerTask('default', ['startup']);
+};
index 9e379a4..58b92b0 100644 (file)
@@ -196,6 +196,10 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_logs', new lang_string('generallogs','backup'), new lang_string('configgenerallogs','backup'), array('value'=>0, 'locked'=>0)));
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_histories', new lang_string('generalhistories','backup'), new lang_string('configgeneralhistories','backup'), array('value'=>0, 'locked'=>0)));
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_questionbank', new lang_string('generalquestionbank','backup'), new lang_string('configgeneralquestionbank','backup'), array('value'=>1, 'locked'=>0)));
+    $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_groups',
+            new lang_string('generalgroups', 'backup'), new lang_string('configgeneralgroups', 'backup'),
+            array('value' => 1, 'locked' => 0)));
+
     $ADMIN->add('backups', $temp);
 
     // Create a page for general import configuration and defaults.
@@ -271,6 +275,8 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
     $temp->add(new admin_setting_configcheckbox('backup/backup_auto_logs', new lang_string('generallogs', 'backup'), new lang_string('configgenerallogs', 'backup'), 0));
     $temp->add(new admin_setting_configcheckbox('backup/backup_auto_histories', new lang_string('generalhistories','backup'), new lang_string('configgeneralhistories','backup'), 0));
     $temp->add(new admin_setting_configcheckbox('backup/backup_auto_questionbank', new lang_string('generalquestionbank','backup'), new lang_string('configgeneralquestionbank','backup'), 1));
+    $temp->add(new admin_setting_configcheckbox('backup/backup_auto_groups', new lang_string('generalgroups', 'backup'),
+            new lang_string('configgeneralgroups', 'backup'), 1));
 
     //$temp->add(new admin_setting_configcheckbox('backup/backup_auto_messages', new lang_string('messages', 'message'), new lang_string('backupmessageshelp','message'), 0));
     //$temp->add(new admin_setting_configcheckbox('backup/backup_auto_blogs', new lang_string('blogs', 'blog'), new lang_string('backupblogshelp','blog'), 0));
index 06f815e..d3eee58 100644 (file)
@@ -156,7 +156,7 @@ class controller {
         $updateablelangs = array();
         foreach ($currentlangs as $clang) {
             if (!array_key_exists($clang, $md5array)) {
-                $noticeok[] = get_string('langpackupdateskipped', 'tool_langimport', $clang);
+                $this->info[] = get_string('langpackupdateskipped', 'tool_langimport', $clang);
                 continue;
             }
             $dest1 = $CFG->dataroot.'/lang/'.$clang;
@@ -175,36 +175,10 @@ class controller {
             }
         }
 
-        // Clean-up currently installed versions of the packs.
-        foreach ($neededlangs as $packindex => $pack) {
-            if ($pack == 'en') {
-                continue;
-            }
-
-            // Delete old directories.
-            $dest1 = $CFG->dataroot.'/lang/'.$pack;
-            $dest2 = $CFG->dirroot.'/lang/'.$pack;
-            if (file_exists($dest1)) {
-                if (!remove_dir($dest1)) {
-                    $noticeerror[] = 'Could not delete old directory '.$dest1.', update of '.$pack
-                        .' failed, please check permissions.';
-                    unset($neededlangs[$packindex]);
-                    continue;
-                }
-            }
-            if (file_exists($dest2)) {
-                if (!remove_dir($dest2)) {
-                    $noticeerror[] = 'Could not delete old directory '.$dest2.', update of '.$pack
-                        .' failed, please check permissions.';
-                    unset($neededlangs[$packindex]);
-                    continue;
-                }
-            }
-        }
-
         try {
             $updated = $this->install_languagepacks($neededlangs, true);
         } catch (\moodle_exception $e) {
+            $this->errors[] = 'An exception occurred while installing language packs: ' . $e->getMessage();
             return false;
         }
 
index 9465bf4..3060292 100644 (file)
@@ -157,6 +157,13 @@ class external_service_form extends moodleform {
 
         $errors = parent::validation($data, $files);
 
+        // Add field validation check for duplicate name.
+        if (!empty($data['name'])) {
+            if ($DB->record_exists('external_services', array('name' => $data['name']))) {
+                $errors['name'] = get_string('nameexists', 'webservice');
+            }
+        }
+
         // Add field validation check for duplicate shortname.
         // Allow duplicated "empty" shortnames.
         if (!empty($data['shortname'])) {
index 02b3529..ddfe96f 100644 (file)
@@ -143,7 +143,8 @@ class auth_plugin_shibboleth extends auth_plugin_base {
         $configarray = (array) $this->config;
 
         $moodleattributes = array();
-        foreach ($this->userfields as $field) {
+        $userfields = array_merge($this->userfields, $this->get_custom_user_profile_fields());
+        foreach ($userfields as $field) {
             if (isset($configarray["field_map_$field"])) {
                 $moodleattributes[$field] = $configarray["field_map_$field"];
             }
index 9a1854f..101be91 100644 (file)
@@ -138,7 +138,7 @@ urn:mace:organization2:providerID, Example Organization 2, /Shibboleth.sso/WAYF/
 
 <?php
 
-print_auth_lock_options($this->authtype, $user_fields, '<!-- empty help -->', true, false);
+print_auth_lock_options($this->authtype, $user_fields, '<!-- empty help -->', true, false, $this->get_custom_user_profile_fields());
 
 ?>
 </table>
index 95363c9..afd1d40 100644 (file)
@@ -311,17 +311,25 @@ abstract class info {
      * @param int $courseid Target course id
      * @param \base_logger $logger Logger for any warnings
      * @param int $dateoffset Date offset to be added to any dates (0 = none)
+     * @param \base_task $task Restore task
      */
-    public function update_after_restore($restoreid, $courseid, \base_logger $logger, $dateoffset) {
+    public function update_after_restore($restoreid, $courseid, \base_logger $logger,
+            $dateoffset, \base_task $task) {
         $tree = $this->get_availability_tree();
         // Set static data for use by get_restore_date_offset function.
-        self::$restoreinfo = array('restoreid' => $restoreid, 'dateoffset' => $dateoffset);
+        self::$restoreinfo = array('restoreid' => $restoreid, 'dateoffset' => $dateoffset,
+                'task' => $task);
         $changed = $tree->update_after_restore($restoreid, $courseid, $logger,
                 $this->get_thing_name());
         if ($changed) {
             // Save modified data.
-            $structure = $tree->save();
-            $this->set_in_database(json_encode($structure));
+            if ($tree->is_empty()) {
+                // If the tree is empty, but the tree has changed, remove this condition.
+                $this->set_in_database(null);
+            } else {
+                $structure = $tree->save();
+                $this->set_in_database(json_encode($structure));
+            }
         }
     }
 
@@ -343,6 +351,24 @@ abstract class info {
         return self::$restoreinfo['dateoffset'];
     }
 
+    /**
+     * Gets the restore task (specifically, the task that calls the
+     * update_after_restore method) for the current restore.
+     *
+     * @param string $restoreid Restore identifier
+     * @return \base_task Restore task
+     * @throws coding_exception If not in a restore (or not in that restore)
+     */
+    public static function get_restore_task($restoreid) {
+        if (!self::$restoreinfo) {
+            throw new coding_exception('Only valid during restore');
+        }
+        if (self::$restoreinfo['restoreid'] !== $restoreid) {
+            throw new coding_exception('Data not available for that restore id');
+        }
+        return self::$restoreinfo['task'];
+    }
+
     /**
      * Obtains the name of the item (cm_info or section_info, at present) that
      * this is controlling availability of. Name should be formatted ready
index e6383ed..8849320 100644 (file)
@@ -663,10 +663,18 @@ class tree extends tree_node {
     public function update_after_restore($restoreid, $courseid,
             \base_logger $logger, $name) {
         $changed = false;
-        foreach ($this->children as $child) {
-            $thischanged = $child->update_after_restore($restoreid, $courseid,
-                    $logger, $name);
-            $changed = $changed || $thischanged;
+        foreach ($this->children as $index => $child) {
+            if ($child->include_after_restore($restoreid, $courseid, $logger, $name,
+                    info::get_restore_task($restoreid))) {
+                $thischanged = $child->update_after_restore($restoreid, $courseid,
+                        $logger, $name);
+                $changed = $changed || $thischanged;
+            } else {
+                unset($this->children[$index]);
+                unset($this->showchildren[$index]);
+                $this->showchildren = array_values($this->showchildren);
+                $changed = true;
+            }
         }
         return $changed;
     }
index 0c3e0e4..2b1ddc0 100644 (file)
@@ -82,13 +82,34 @@ abstract class tree_node {
      */
     public abstract function save();
 
+    /**
+     * Checks whether this node should be included after restore or not. The
+     * node may be removed depending on restore settings, which you can get from
+     * the $task object.
+     *
+     * By default nodes are still included after restore.
+     *
+     * @param string $restoreid Restore ID
+     * @param int $courseid ID of target course
+     * @param \base_logger $logger Logger for any warnings
+     * @param string $name Name of this item (for use in warning messages)
+     * @param \base_task $task Current restore task
+     * @return bool True if there was any change
+     */
+    public function include_after_restore($restoreid, $courseid, \base_logger $logger, $name,
+            \base_task $task) {
+        return true;
+    }
+
     /**
      * Updates this node after restore, returning true if anything changed.
      * The default behaviour is simply to return false. If there is a problem
      * with the update, $logger can be used to output a warning.
      *
      * Note: If you need information about the date offset, call
-     * \core_availability\info::get_restore_date_offset($restoreid).
+     * \core_availability\info::get_restore_date_offset($restoreid). For
+     * information on the restoring task and its settings, call
+     * \core_availability\info::get_restore_task($restoreid).
      *
      * @param string $restoreid Restore ID
      * @param int $courseid ID of target course
index 2445cdc..46e3934 100644 (file)
@@ -127,6 +127,23 @@ class condition extends \core_availability\condition {
         return $this->groupid ? '#' . $this->groupid : 'any';
     }
 
+    /**
+     * Include this condition only if we are including groups in restore, or
+     * if it's a generic 'same activity' one.
+     *
+     * @param int $restoreid The restore Id.
+     * @param int $courseid The ID of the course.
+     * @param base_logger $logger The logger being used.
+     * @param string $name Name of item being restored.
+     * @param base_task $task The task being performed.
+     *
+     * @return Integer groupid
+     */
+    public function include_after_restore($restoreid, $courseid, \base_logger $logger,
+            $name, \base_task $task) {
+        return !$this->groupid || $task->get_setting_value('groups');
+    }
+
     public function update_after_restore($restoreid, $courseid, \base_logger $logger, $name) {
         global $DB;
         if (!$this->groupid) {
index e0b5997..1fdd928 100644 (file)
@@ -158,6 +158,23 @@ class condition extends \core_availability\condition {
         }
     }
 
+    /**
+     * Include this condition only if we are including groups in restore, or
+     * if it's a generic 'same activity' one.
+     *
+     * @param int $restoreid The restore Id.
+     * @param int $courseid The ID of the course.
+     * @param base_logger $logger The logger being used.
+     * @param string $name Name of item being restored.
+     * @param base_task $task The task being performed.
+     *
+     * @return Integer groupid
+     */
+    public function include_after_restore($restoreid, $courseid, \base_logger $logger,
+            $name, \base_task $task) {
+        return !$this->groupingid || $task->get_setting_value('groups');
+    }
+
     public function update_after_restore($restoreid, $courseid, \base_logger $logger, $name) {
         global $DB;
         if (!$this->groupingid) {
index 1d2dc46..92d3b8d 100644 (file)
@@ -2,6 +2,13 @@ This files describes API changes in /availability/*.
 
 The information here is intended only for developers.
 
+=== 2.9 ===
+
+* Condition plugins can now implement a new include_after_restore function to
+  indicate that they should be removed during the restore process. (This is
+  implemented so that group and grouping conditions are removed if groups are
+  not restored.)
+
 === 2.8 ===
 
 * There is a new API function in the info_module/info_section objects (and
index e39c858..8eac169 100644 (file)
@@ -137,8 +137,10 @@ abstract class backup_activity_task extends backup_task {
         // activity and from its related course_modules record and availability
         $this->add_step(new backup_module_structure_step('module_info', 'module.xml'));
 
-        // Annotate the groups used in already annotated groupings
-        $this->add_step(new backup_annotate_groups_from_groupings('annotate_groups'));
+        // Annotate the groups used in already annotated groupings if groups are to be backed up.
+        if ($this->get_setting_value('groups')) {
+            $this->add_step(new backup_annotate_groups_from_groupings('annotate_groups'));
+        }
 
         // Here we add all the common steps for any activity and, in the point of interest
         // we call to define_my_steps() is order to get the particular ones inserted in place.
index 5ed54e9..c7b64b4 100644 (file)
@@ -83,15 +83,18 @@ class backup_course_task extends backup_task {
         // Annotate enrolment custom fields.
         $this->add_step(new backup_enrolments_execution_step('annotate_enrol_custom_fields'));
 
-        // Annotate all the groups and groupings belonging to the course
-        $this->add_step(new backup_annotate_course_groups_and_groupings('annotate_course_groups'));
+        // Annotate all the groups and groupings belonging to the course. This can be optional.
+        if ($this->get_setting_value('groups')) {
+            $this->add_step(new backup_annotate_course_groups_and_groupings('annotate_course_groups'));
+        }
 
         // Annotate the groups used in already annotated groupings (note this may be
         // unnecessary now that we are annotating all the course groups and groupings in the
-        // step above. But we keep it working in case we decide, someday, to introduce one
-        // setting to transform the step above into an optional one. This is here to support
-        // course->defaultgroupingid
-        $this->add_step(new backup_annotate_groups_from_groupings('annotate_groups_from_groupings'));
+        // step above). This is here to support course->defaultgroupingid.
+        // This may not be required to annotate if groups are not being backed up.
+        if ($this->get_setting_value('groups')) {
+            $this->add_step(new backup_annotate_groups_from_groupings('annotate_groups_from_groupings'));
+        }
 
         // Annotate the question_categories belonging to the course context (conditionally).
         if ($this->get_setting_value('questionbank')) {
index 0a2df8a..45e792d 100644 (file)
@@ -159,5 +159,9 @@ class backup_root_task extends backup_task {
         $questionbank = new backup_generic_setting('questionbank', base_setting::IS_BOOLEAN, true);
         $questionbank->set_ui(new backup_setting_ui_checkbox($questionbank, get_string('rootsettingquestionbank', 'backup')));
         $this->add_setting($questionbank);
+
+        $groups = new backup_groups_setting('groups', base_setting::IS_BOOLEAN, true);
+        $groups->set_ui(new backup_setting_ui_checkbox($groups, get_string('rootsettinggroups', 'backup')));
+        $this->add_setting($groups);
     }
 }
index b3704ab..f799e4a 100644 (file)
@@ -65,6 +65,15 @@ class backup_filename_setting extends backup_generic_setting {
  */
 class backup_users_setting extends backup_generic_setting {}
 
+/**
+ * root setting to control if backup will include group information depends on @backup_users_setting
+ *
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2014 Matt Sammarco
+ */
+class backup_groups_setting extends backup_generic_setting {
+}
+
 /**
  * root setting to control if backup will include activities or no.
  * A lot of other settings (_included at activity levels)
index 78507d2..c761eba 100644 (file)
@@ -1149,8 +1149,10 @@ class backup_groups_structure_step extends backup_structure_step {
 
     protected function define_structure() {
 
-        // To know if we are including users
-        $users = $this->get_setting_value('users');
+        // To know if we are including users.
+        $userinfo = $this->get_setting_value('users');
+        // To know if we are including groups and groupings.
+        $groupinfo = $this->get_setting_value('groups');
 
         // Define each element separated
 
@@ -1190,27 +1192,29 @@ class backup_groups_structure_step extends backup_structure_step {
 
         // Define sources
 
-        $group->set_source_sql("
-            SELECT g.*
-              FROM {groups} g
-              JOIN {backup_ids_temp} bi ON g.id = bi.itemid
-             WHERE bi.backupid = ?
-               AND bi.itemname = 'groupfinal'", array(backup::VAR_BACKUPID));
-
-        // This only happens if we are including users
-        if ($users) {
-            $member->set_source_table('groups_members', array('groupid' => backup::VAR_PARENTID));
+        // This only happens if we are including groups/groupings.
+        if ($groupinfo) {
+            $group->set_source_sql("
+                SELECT g.*
+                  FROM {groups} g
+                  JOIN {backup_ids_temp} bi ON g.id = bi.itemid
+                 WHERE bi.backupid = ?
+                   AND bi.itemname = 'groupfinal'", array(backup::VAR_BACKUPID));
+
+            $grouping->set_source_sql("
+                SELECT g.*
+                  FROM {groupings} g
+                  JOIN {backup_ids_temp} bi ON g.id = bi.itemid
+                 WHERE bi.backupid = ?
+                   AND bi.itemname = 'groupingfinal'", array(backup::VAR_BACKUPID));
+            $groupinggroup->set_source_table('groupings_groups', array('groupingid' => backup::VAR_PARENTID));
+
+            // This only happens if we are including users.
+            if ($userinfo) {
+                $member->set_source_table('groups_members', array('groupid' => backup::VAR_PARENTID));
+            }
         }
 
-        $grouping->set_source_sql("
-            SELECT g.*
-              FROM {groupings} g
-              JOIN {backup_ids_temp} bi ON g.id = bi.itemid
-             WHERE bi.backupid = ?
-               AND bi.itemname = 'groupingfinal'", array(backup::VAR_BACKUPID));
-
-        $groupinggroup->set_source_table('groupings_groups', array('groupingid' => backup::VAR_PARENTID));
-
         // Define id annotations (as final)
 
         $member->annotate_ids('userfinal', 'userid');
@@ -1241,7 +1245,7 @@ class backup_users_structure_step extends backup_structure_step {
         // To know if we are including role assignments
         $roleassignments = $this->get_setting_value('role_assignments');
 
-        // Define each element separated
+        // Define each element separate.
 
         $users = new backup_nested_element('users');
 
index 73384df..922e203 100644 (file)
@@ -249,5 +249,22 @@ class restore_root_task extends restore_task {
         // The restore does not process the grade histories when some activities are ignored.
         // So let's define a dependency to prevent false expectations from our users.
         $activities->add_dependency($gradehistories);
+
+        // Define groups and groupings.
+        $defaultvalue = false;
+        $changeable = false;
+        if (isset($rootsettings['groups']) && $rootsettings['groups']) { // Only enabled when available.
+            $defaultvalue = true;
+            $changeable = true;
+        } else if (!isset($rootsettings['groups'])) {
+            // It is likely this is an older backup that does not contain information on the group setting,
+            // in which case groups should be restored and this setting can be changed.
+            $defaultvalue = true;
+            $changeable = true;
+        }
+        $groups = new restore_groups_setting('groups', base_setting::IS_BOOLEAN, $defaultvalue);
+        $groups->set_ui(new backup_setting_ui_checkbox($groups, get_string('rootsettinggroups', 'backup')));
+        $groups->get_ui()->set_changeable($changeable);
+        $this->add_setting($groups);
     }
 }
index 9568637..3ac0e2b 100644 (file)
@@ -43,6 +43,15 @@ class restore_generic_setting extends root_backup_setting {}
  */
 class restore_users_setting extends restore_generic_setting {}
 
+/**
+ * root setting to control if restore will create groups/grouping information. Depends on @restore_users_setting
+ *
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2014 Matt Sammarco
+ */
+class restore_groups_setting extends restore_generic_setting {
+}
+
 /**
  * root setting to control if restore will create role assignments
  * or no (any level), depends of @restore_users_setting
index d0b9a3a..d779cfa 100644 (file)
@@ -683,7 +683,7 @@ class restore_update_availability extends restore_execution_step {
             if (!is_null($section->availability)) {
                 $info = new \core_availability\info_section($section);
                 $info->update_after_restore($this->get_restoreid(),
-                        $this->get_courseid(), $this->get_logger(), $dateoffset);
+                        $this->get_courseid(), $this->get_logger(), $dateoffset, $this->task);
             }
         }
         $rs->close();
@@ -703,7 +703,7 @@ class restore_update_availability extends restore_execution_step {
             if (!is_null($cm->availability)) {
                 $info = new \core_availability\info_module($cm);
                 $info->update_after_restore($this->get_restoreid(),
-                        $this->get_courseid(), $this->get_logger(), $dateoffset);
+                        $this->get_courseid(), $this->get_logger(), $dateoffset, $this->task);
             }
         }
         $rs->close();
@@ -937,10 +937,13 @@ class restore_groups_structure_step extends restore_structure_step {
 
         $paths = array(); // Add paths here
 
-        $paths[] = new restore_path_element('group', '/groups/group');
-        $paths[] = new restore_path_element('grouping', '/groups/groupings/grouping');
-        $paths[] = new restore_path_element('grouping_group', '/groups/groupings/grouping/grouping_groups/grouping_group');
-
+        // Do not include group/groupings information if not requested.
+        $groupinfo = $this->get_setting_value('groups');
+        if ($groupinfo) {
+            $paths[] = new restore_path_element('group', '/groups/group');
+            $paths[] = new restore_path_element('grouping', '/groups/groupings/grouping');
+            $paths[] = new restore_path_element('grouping_group', '/groups/groupings/grouping/grouping_groups/grouping_group');
+        }
         return $paths;
     }
 
@@ -1069,7 +1072,7 @@ class restore_groups_members_structure_step extends restore_structure_step {
 
         $paths = array(); // Add paths here
 
-        if ($this->get_setting_value('users')) {
+        if ($this->get_setting_value('groups') && $this->get_setting_value('users')) {
             $paths[] = new restore_path_element('group', '/groups/group');
             $paths[] = new restore_path_element('member', '/groups/group/group_members/group_member');
         }
index e7456ce..06aa423 100644 (file)
@@ -574,7 +574,8 @@ abstract class backup_controller_dbops extends backup_dbops {
             'backup_auto_userscompletion'    => 'userscompletion',
             'backup_auto_logs'               => 'logs',
             'backup_auto_histories'          => 'grade_histories',
-            'backup_auto_questionbank'       => 'questionbank'
+            'backup_auto_questionbank'       => 'questionbank',
+            'backup_auto_groups'             => 'groups'
         );
         $plan = $controller->get_plan();
         foreach ($settings as $config => $settingname) {
@@ -612,7 +613,8 @@ abstract class backup_controller_dbops extends backup_dbops {
             'backup_general_userscompletion'    => 'userscompletion',
             'backup_general_logs'               => 'logs',
             'backup_general_histories'          => 'grade_histories',
-            'backup_general_questionbank'       => 'questionbank'
+            'backup_general_questionbank'       => 'questionbank',
+            'backup_general_groups'             => 'groups'
         );
         $plan = $controller->get_plan();
         foreach ($settings as $config=>$settingname) {
diff --git a/badges/tests/events_test.php b/badges/tests/events_test.php
new file mode 100644 (file)
index 0000000..833cf09
--- /dev/null
@@ -0,0 +1,58 @@
+<?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/>.
+/**
+ * Badge events tests.
+ *
+ * @package    core_badges
+ * @copyright  2015 onwards Simey Lameze <simey@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 . '/badges/tests/badgeslib_test.php');
+
+/**
+ * Badge events tests class.
+ *
+ * @package    core_badges
+ * @copyright  2015 onwards Simey Lameze <simey@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_badges_events_testcase extends core_badges_badgeslib_testcase {
+
+    /**
+     * Test badge awarded event.
+     */
+    public function test_badge_awarded() {
+
+        $systemcontext = context_system::instance();
+
+        $sink = $this->redirectEvents();
+
+        $badge = new badge($this->badgeid);
+        $badge->issue($this->user->id, true);
+        $badge->is_issued($this->user->id);
+        $events = $sink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+        $this->assertInstanceOf('\core\event\badge_awarded', $event);
+        $this->assertEquals($this->badgeid, $event->objectid);
+        $this->assertEquals($this->user->id, $event->relateduserid);
+        $this->assertEquals($systemcontext, $event->get_context());
+
+        $sink->close();
+    }
+}
\ No newline at end of file
index a78eaa7..ec9c063 100644 (file)
@@ -25,7 +25,7 @@
 $string['activityoverview'] = 'You have {$a}s that need attention';
 $string['alwaysshowall'] = 'Always show all';
 $string['collapseall'] = 'Collapse all course lists';
-$string['configotherexpanded'] = 'If enabled, other courses will be expanded by default unless overriden by user preferences.';
+$string['configotherexpanded'] = 'If enabled, other courses will be expanded by default unless overridden by user preferences.';
 $string['configpreservestates'] = 'If enabled, the collapsed/expanded states set by the user are stored and used on each load.';
 $string['course_overview:addinstance'] = 'Add a new course overview block';
 $string['course_overview:myaddinstance'] = 'Add a new course overview block to My home';
index b71303c..b0e0e57 100644 (file)
@@ -56,3 +56,16 @@ Feature: Add and configure blocks throughout the site
     And I follow "Course 1"
     And I follow "Turn editing on"
     Then I should see "Assign roles in Search forums block"
+
+  @javascript
+  Scenario: Blocks can safely be customised
+    Given I log in as "admin"
+    And I click on "My home" "link" in the "Navigation" "block"
+    And I press "Customise this page"
+    And I add the "HTML" block
+    And I configure the "(new HTML block)" block
+    And I set the following fields to these values:
+      | Block title | Foo " onload="document.getElementsByTagName('body')[0].remove()" alt="
+      | Content     | Example
+    When I press "Save changes"
+    Then I should see "Course overview"
index bbe8992..6f41d19 100644 (file)
@@ -169,7 +169,7 @@ if ($editform->is_cancelled()) {
                 if ($plugin = enrol_get_plugin($instance->enrol)) {
                     if ($plugin->get_manual_enrol_link($instance)) {
                         // We know that the ajax enrol UI will have an option to enrol.
-                        $courseurl = new moodle_url('/enrol/users.php', array('id' => $course->id));
+                        $courseurl = new moodle_url('/enrol/users.php', array('id' => $course->id, 'newcourse' => 1));
                         break;
                     }
                 }
index ff3dbfa..0399d4f 100644 (file)
@@ -77,11 +77,14 @@ class core_course_external extends external_api {
         //retrieve the course
         $course = $DB->get_record('course', array('id' => $params['courseid']), '*', MUST_EXIST);
 
-        //check course format exist
-        if (!file_exists($CFG->dirroot . '/course/format/' . $course->format . '/lib.php')) {
-            throw new moodle_exception('cannotgetcoursecontents', 'webservice', '', null, get_string('courseformatnotfound', 'error', '', $course->format));
-        } else {
-            require_once($CFG->dirroot . '/course/format/' . $course->format . '/lib.php');
+        if ($course->id != SITEID) {
+            // Check course format exist.
+            if (!file_exists($CFG->dirroot . '/course/format/' . $course->format . '/lib.php')) {
+                throw new moodle_exception('cannotgetcoursecontents', 'webservice', '', null,
+                                            get_string('courseformatnotfound', 'error', $course->format));
+            } else {
+                require_once($CFG->dirroot . '/course/format/' . $course->format . '/lib.php');
+            }
         }
 
         // now security checks
index b1cd895..72b0cb2 100644 (file)
@@ -45,7 +45,7 @@ class core_enrol_renderer extends plugin_renderer_base {
         $buttons = $table->get_manual_enrol_buttons();
         $buttonhtml = '';
         if (count($buttons) > 0) {
-            $buttonhtml .= html_writer::start_tag('div', array('class' => 'enrol_user_buttons'));
+            $buttonhtml .= html_writer::start_tag('div', array('class' => 'enrol_user_buttons enrol-users-page-action'));
             foreach ($buttons as $button) {
                 $buttonhtml .= $this->render($button);
             }
index 99e5133..ac2b68d 100644 (file)
@@ -35,10 +35,11 @@ $search  = optional_param('search', '', PARAM_RAW);
 $role    = optional_param('role', 0, PARAM_INT);
 $fgroup  = optional_param('filtergroup', 0, PARAM_INT);
 $status  = optional_param('status', -1, PARAM_INT);
+$newcourse = optional_param('newcourse', false, PARAM_BOOL);
 
 // When users reset the form, redirect back to first page without other params.
 if (optional_param('resetbutton', '', PARAM_RAW) !== '') {
-    redirect('users.php?id=' . $id);
+    redirect('users.php?id=' . $id . '&newcourse=' . $newcourse);
 }
 
 $course = $DB->get_record('course', array('id'=>$id), '*', MUST_EXIST);
@@ -54,7 +55,7 @@ $PAGE->set_pagelayout('admin');
 
 $manager = new course_enrolment_manager($PAGE, $course, $filter, $role, $search, $fgroup, $status);
 $table = new course_enrolment_users_table($manager, $PAGE);
-$PAGE->set_url('/enrol/users.php', $manager->get_url_params()+$table->get_url_params());
+$PAGE->set_url('/enrol/users.php', $manager->get_url_params()+$table->get_url_params()+array('newcourse' => $newcourse));
 navigation_node::override_active_url(new moodle_url('/enrol/users.php', array('id' => $id)));
 
 // Check if there is an action to take
@@ -217,7 +218,7 @@ if (!has_capability('moodle/course:viewhiddenuserfields', $context)) {
     }
 }
 
-$filterform = new enrol_users_filter_form('users.php', array('manager' => $manager, 'id' => $id),
+$filterform = new enrol_users_filter_form('users.php', array('manager' => $manager, 'id' => $id, 'newcourse' => $newcourse),
         'get', '', array('id' => 'filterform'));
 $filterform->set_data(array('search' => $search, 'ifilter' => $filter, 'role' => $role, 'filtergroup' => $fgroup));
 
@@ -240,4 +241,8 @@ $PAGE->set_heading($PAGE->title);
 echo $OUTPUT->header();
 echo $OUTPUT->heading(get_string('enrolledusers', 'enrol'));
 echo $renderer->render_course_enrolment_users_table($table, $filterform);
+if ($newcourse == 1) {
+    echo $OUTPUT->single_button(new moodle_url('/course/view.php', array('id' => $id)),
+    get_string('proceedtocourse', 'enrol'), 'GET', array('class' => 'enrol-users-page-action'));
+}
 echo $OUTPUT->footer();
index a854d80..ab55530 100644 (file)
@@ -190,5 +190,7 @@ class enrol_users_filter_form extends moodleform {
         // Add hidden fields required by page.
         $mform->addElement('hidden', 'id', $this->_customdata['id']);
         $mform->setType('id', PARAM_INT);
+        $mform->addElement('hidden', 'newcourse', $this->_customdata['newcourse']);
+        $mform->setType('newcourse', PARAM_BOOL);
     }
 }
index 538d03e..354dbe1 100644 (file)
@@ -134,10 +134,9 @@ class filter_urltolink extends moodle_text_filter {
 
         // Lookbehind assertions.
         // Is not HTML attribute or CSS URL property. Unfortunately legit text like "url(http://...)" will not be a link.
-        $lookbehindstart = "(?<!=[\"']|\burl\([\"' ]|\burl\()";
         $lookbehindend = "(?<![]),.;])";
 
-        $regex = "$lookbehindstart$urlstart((?:$domainsegment\.)+$domainsegment|$numericip)" .
+        $regex = "$urlstart((?:$domainsegment\.)+$domainsegment|$numericip)" .
                 "($port?$path$querystring?$fragment?)$lookbehindend";
         if ($unicoderegexp) {
             $regex = '#' . $regex . '#ui';
@@ -145,7 +144,38 @@ class filter_urltolink extends moodle_text_filter {
             $regex = '#' . preg_replace(array('\pLl', '\PL'), 'a-z', $regex) . '#i';
         }
 
-        $text = preg_replace($regex, '<a href="http$1://$2$3$4" class="_blanktarget">$0</a>', $text);
+        // Locate any HTML tags.
+        $matches = preg_split('/(<[^<|>]*>)/i', $text, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
+
+        // Iterate through the tokenized text to handle chunks (html and content).
+        foreach ($matches as $idx => $chunk) {
+            // Nothing to do. We skip completely any html chunk.
+            if (strpos(trim($chunk), '<') === 0) {
+                continue;
+            }
+
+            // Nothing to do. We skip any content chunk having any of these attributes.
+            if (preg_match('#(background=")|(action=")|(style="background)|(href=")|(src=")|(url [(])#', $chunk)) {
+                continue;
+            }
+
+            // Arrived here, we want to process every word in this chunk.
+            $text = $chunk;
+            $words = explode(' ', $text);
+
+            foreach ($words as $idx2 => $word) {
+                // ReDoS protection. Stop processing if a word is too large.
+                if (strlen($word) < 4096) {
+                    $words[$idx2] = preg_replace($regex, '<a href="http$1://$2$3$4" class="_blanktarget">$0</a>', $word);
+                }
+            }
+            $text = implode(' ', $words);
+
+            // Copy the result back to the array.
+            $matches[$idx] = $text;
+        }
+
+        $text = implode('', $matches);
 
         if (!empty($ignoretags)) {
             $ignoretags = array_reverse($ignoretags); /// Reversed so "progressive" str_replace() will solve some nesting problems.
index 58bd4a3..0ae7f89 100644 (file)
@@ -29,9 +29,13 @@ global $CFG;
 require_once($CFG->dirroot . '/filter/urltolink/filter.php'); // Include the code to test
 
 
-class filter_urltolink_testcase extends basic_testcase {
+class filter_urltolink_filter_testcase extends basic_testcase {
 
     function get_convert_urls_into_links_test_cases() {
+        // Create a 4095 and 4096 long URLs.
+        $superlong4095 = str_pad('http://www.superlong4095.com?this=something', 4095, 'a');
+        $superlong4096 = str_pad('http://www.superlong4096.com?this=something', 4096, 'a');
+
         $texts = array (
             //just a url
             'http://moodle.org - URL' => '<a href="http://moodle.org" class="_blanktarget">http://moodle.org</a> - URL',
@@ -130,6 +134,7 @@ class filter_urltolink_testcase extends basic_testcase {
             '<td background="http://moodle.org">&nbsp;</td>' => '<td background="http://moodle.org">&nbsp;</td>',
             '<td background="www.moodle.org">&nbsp;</td>' => '<td background="www.moodle.org">&nbsp;</td>',
             '<form name="input" action="http://moodle.org/submit.asp" method="get">'=>'<form name="input" action="http://moodle.org/submit.asp" method="get">',
+            '<input type="submit" value="Go to http://moodle.org">' => '<input type="submit" value="Go to http://moodle.org">',
             '<td background="https://www.moodle.org">&nbsp;</td>' => '<td background="https://www.moodle.org">&nbsp;</td>',
             // CSS URLs.
             '<table style="background-image: url(\'http://moodle.org/pic.jpg\');">' => '<table style="background-image: url(\'http://moodle.org/pic.jpg\');">',
@@ -148,6 +153,27 @@ class filter_urltolink_testcase extends basic_testcase {
             //Encoded URLs in the query
             'URL: http://127.0.0.1/path/to?param=value_with%28parenthesis%29&param2=1' => 'URL: <a href="http://127.0.0.1/path/to?param=value_with%28parenthesis%29&param2=1" class="_blanktarget">http://127.0.0.1/path/to?param=value_with%28parenthesis%29&param2=1</a>',
             'URL: www.localhost.com/path/to?param=value_with%28parenthesis%29&param2=1' => 'URL: <a href="http://www.localhost.com/path/to?param=value_with%28parenthesis%29&param2=1" class="_blanktarget">www.localhost.com/path/to?param=value_with%28parenthesis%29&param2=1</a>',
+            // Test URL less than 4096 characters in size is converted to link.
+            'URL: ' . $superlong4095 => 'URL: <a href="' . $superlong4095 . '" class="_blanktarget">' . $superlong4095 . '</a>',
+            // Test URL equal to or greater than 4096 characters in size is not converted to link.
+            'URL: ' . $superlong4096 => 'URL: ' . $superlong4096,
+            // Testing URL within a span tag.
+            'URL: <span style="kasd"> my link to http://google.com </span>' => 'URL: <span style="kasd"> my link to <a href="http://google.com" class="_blanktarget">http://google.com</a> </span>',
+            // Nested tags test.
+            '<b><i>www.google.com</i></b>' => '<b><i><a href="http://www.google.com" class="_blanktarget">www.google.com</a></i></b>',
+            '<input type="submit" value="Go to http://moodle.org">' => '<input type="submit" value="Go to http://moodle.org">',
+            // Test realistic content.
+            '<p><span style="color: rgb(37, 37, 37); font-family: sans-serif; line-height: 22.3999996185303px;">Lorem ipsum amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut http://google.com aliquip ex ea <a href="http://google.com">commodo consequat</a>. Duis aute irure in reprehenderit in excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia https://docs.google.com/document/d/BrokenLinkPleaseAyacDHc_Ov8aoskoSVQsfmLHP_jYAkRMk/edit?usp=sharing https://docs.google.com/document/d/BrokenLinkPleaseAyacDHc_Ov8aoskoSVQsfmLHP_jYAkRMk/edit?usp=sharing mollit anim id est laborum.</span><br></p>'
+            =>
+            '<p><span style="color: rgb(37, 37, 37); font-family: sans-serif; line-height: 22.3999996185303px;">Lorem ipsum amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut <a href="http://google.com" class="_blanktarget">http://google.com</a> aliquip ex ea <a href="http://google.com">commodo consequat</a>. Duis aute irure in reprehenderit in excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia <a href="https://docs.google.com/document/d/BrokenLinkPleaseAyacDHc_Ov8aoskoSVQsfmLHP_jYAkRMk/edit?usp=sharing" class="_blanktarget">https://docs.google.com/document/d/BrokenLinkPleaseAyacDHc_Ov8aoskoSVQsfmLHP_jYAkRMk/edit?usp=sharing</a> <a href="https://docs.google.com/document/d/BrokenLinkPleaseAyacDHc_Ov8aoskoSVQsfmLHP_jYAkRMk/edit?usp=sharing" class="_blanktarget">https://docs.google.com/document/d/BrokenLinkPleaseAyacDHc_Ov8aoskoSVQsfmLHP_jYAkRMk/edit?usp=sharing</a> mollit anim id est laborum.</span><br></p>',
+            // Test some broken html.
+            '5 < 10 www.google.com <a href="hi.com">im a link</a>' => '5 < 10 <a href="http://www.google.com" class="_blanktarget">www.google.com</a> <a href="hi.com">im a link</a>',
+            'h3 (www.styles.com/h3) < h1 (www.styles.com/h1)' => 'h3 (<a href="http://www.styles.com/h3" class="_blanktarget">www.styles.com/h3</a>) < h1 (<a href="http://www.styles.com/h1" class="_blanktarget">www.styles.com/h1</a>)',
+            '<p>text www.moodle.org&lt;/p> text' => '<p>text <a href="http://www.moodle.org" class="_blanktarget">www.moodle.org</a>&lt;/p> text',
+            // Some more urls.
+            '<link rel="search" type="application/opensearchdescription+xml" href="/osd.jsp" title="Peer review - Moodle Tracker"/>' => '<link rel="search" type="application/opensearchdescription+xml" href="/osd.jsp" title="Peer review - Moodle Tracker"/>',
+            '<a href="https://docs.moodle.org/dev/Main_Page"></a><span>www.google.com</span><span class="placeholder"></span>' => '<a href="https://docs.moodle.org/dev/Main_Page"></a><span><a href="http://www.google.com" class="_blanktarget">www.google.com</a></span><span class="placeholder"></span>',
+            'http://nolandforzombies.com <a href="zombiesFTW.com">Zombies FTW</a> http://aliens.org' => '<a href="http://nolandforzombies.com" class="_blanktarget">http://nolandforzombies.com</a> <a href="zombiesFTW.com">Zombies FTW</a> <a href="http://aliens.org" class="_blanktarget">http://aliens.org</a>',
             //URLs in Javascript. Commented out as part of MDL-21183
             //'var url="http://moodle.org";'=>'var url="http://moodle.org";',
             //'var url = "http://moodle.org";'=>'var url = "http://moodle.org";',
index f02b748..45eadb3 100644 (file)
@@ -1154,6 +1154,115 @@ class core_group_external extends external_api {
         return null;
     }
 
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 2.9
+     */
+    public static function get_course_user_groups_parameters() {
+        return new external_function_parameters(
+            array(
+                'courseid' => new external_value(PARAM_INT, 'id of course'),
+                'userid' => new external_value(PARAM_INT, 'id of user'),
+                'groupingid' => new external_value(PARAM_INT, 'returns only groups in the specified grouping', VALUE_DEFAULT, 0)
+            )
+        );
+    }
+
+    /**
+     * Get all groups in the specified course for the specified user.
+     *
+     * @throws moodle_exception
+     * @param int $courseid id of course.
+     * @param int $userid id of user.
+     * @param int $groupingid optional returns only groups in the specified grouping.
+     * @return array of group objects (id, name, description, format) and possible warnings.
+     * @since Moodle 2.9
+     */
+    public static function get_course_user_groups($courseid, $userid, $groupingid = 0) {
+        global $USER;
+
+        // Warnings array, it can be empty at the end but is mandatory.
+        $warnings = array();
+
+        $params = array(
+            'courseid' => $courseid,
+            'userid' => $userid,
+            'groupingid' => $groupingid
+        );
+        $params = self::validate_parameters(self::get_course_user_groups_parameters(), $params);
+        $courseid = $params['courseid'];
+        $userid = $params['userid'];
+        $groupingid = $params['groupingid'];
+
+        // Validate course and user. get_course throws an exception if the course does not exists.
+        $course = get_course($courseid);
+        $user = core_user::get_user($userid, 'id', MUST_EXIST);
+
+        // Security checks.
+        $context = context_course::instance($course->id);
+        self::validate_context($context);
+
+         // Check if we have permissions for retrieve the information.
+        if ($user->id != $USER->id) {
+            if (!has_capability('moodle/course:managegroups', $context)) {
+                throw new moodle_exception('accessdenied', 'admin');
+            }
+            // Validate if the user is enrolled in the course.
+            if (!is_enrolled($context, $user->id)) {
+                // We return a warning because the function does not fail for not enrolled users.
+                $warning['item'] = 'course';
+                $warning['itemid'] = $course->id;
+                $warning['warningcode'] = '1';
+                $warning['message'] = "User $user->id is not enrolled in course $course->id";
+                $warnings[] = $warning;
+            }
+        }
+
+        $usergroups = array();
+        if (empty($warnings)) {
+            $groups = groups_get_all_groups($course->id, $user->id, 0, 'g.id, g.name, g.description, g.descriptionformat');
+
+            foreach ($groups as $group) {
+                list($group->description, $group->descriptionformat) =
+                    external_format_text($group->description, $group->descriptionformat,
+                            $context->id, 'group', 'description', $group->id);
+                $usergroups[] = (array)$group;
+            }
+        }
+
+        $results = array(
+            'groups' => $usergroups,
+            'warnings' => $warnings
+        );
+        return $results;
+    }
+
+    /**
+     * Returns description of method result value.
+     *
+     * @return external_description A single structure containing groups and possible warnings.
+     * @since Moodle 2.9
+     */
+    public static function get_course_user_groups_returns() {
+        return new external_single_structure(
+            array(
+                'groups' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'id' => new external_value(PARAM_INT, 'group record id'),
+                            'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'),
+                            'description' => new external_value(PARAM_RAW, 'group description text'),
+                            'descriptionformat' => new external_format_value('description')
+                        )
+                    )
+                ),
+                'warnings' => new external_warnings(),
+            )
+        );
+    }
+
 }
 
 /**
index d3ca01e..a65d930 100644 (file)
@@ -285,4 +285,92 @@ class core_group_externallib_testcase extends externallib_advanced_testcase {
             $this->assertEquals($dbgroup->courseid, $groupcourseid);
         }
     }
+
+    /**
+     * Test get_groups
+     */
+    public function test_get_course_user_groups() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $student1 = self::getDataGenerator()->create_user();
+        $student2 = self::getDataGenerator()->create_user();
+        $teacher = self::getDataGenerator()->create_user();
+
+        $course = self::getDataGenerator()->create_course();
+        $emptycourse = self::getDataGenerator()->create_course();
+
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($student1->id, $course->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($student2->id, $course->id, $studentrole->id);
+
+        $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
+        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
+        $this->getDataGenerator()->enrol_user($teacher->id, $emptycourse->id, $teacherrole->id);
+
+        $group1data = array();
+        $group1data['courseid'] = $course->id;
+        $group1data['name'] = 'Group Test 1';
+        $group1data['description'] = 'Group Test 1 description';
+        $group2data = array();
+        $group2data['courseid'] = $course->id;
+        $group2data['name'] = 'Group Test 2';
+        $group2data['description'] = 'Group Test 2 description';
+        $group1 = self::getDataGenerator()->create_group($group1data);
+        $group2 = self::getDataGenerator()->create_group($group2data);
+
+        groups_add_member($group1->id, $student1->id);
+        groups_add_member($group1->id, $student2->id);
+        groups_add_member($group2->id, $student1->id);
+
+        $this->setUser($student1);
+
+        $groups = core_group_external::get_course_user_groups($course->id, $student1->id);
+        $groups = external_api::clean_returnvalue(core_group_external::get_course_user_groups_returns(), $groups);
+        // Check that I see my groups.
+        $this->assertCount(2, $groups['groups']);
+
+        $this->setUser($student2);
+        $groups = core_group_external::get_course_user_groups($course->id, $student2->id);
+        $groups = external_api::clean_returnvalue(core_group_external::get_course_user_groups_returns(), $groups);
+        // Check that I see my groups.
+        $this->assertCount(1, $groups['groups']);
+
+        $this->assertEquals($group1data['name'], $groups['groups'][0]['name']);
+        $this->assertEquals($group1data['description'], $groups['groups'][0]['description']);
+
+        $this->setUser($teacher);
+        $groups = core_group_external::get_course_user_groups($course->id, $student1->id);
+        $groups = external_api::clean_returnvalue(core_group_external::get_course_user_groups_returns(), $groups);
+        // Check that a teacher can see student groups.
+        $this->assertCount(2, $groups['groups']);
+
+        $groups = core_group_external::get_course_user_groups($course->id, $student2->id);
+        $groups = external_api::clean_returnvalue(core_group_external::get_course_user_groups_returns(), $groups);
+        // Check that a teacher can see student groups.
+        $this->assertCount(1, $groups['groups']);
+
+        // Check permissions.
+        $this->setUser($student1);
+        try {
+            $groups = core_group_external::get_course_user_groups($course->id, $student2->id);
+        } catch (moodle_exception $e) {
+            $this->assertEquals('accessdenied', $e->errorcode);
+        }
+
+        try {
+            $groups = core_group_external::get_course_user_groups($emptycourse->id, $student2->id);
+        } catch (moodle_exception $e) {
+            $this->assertEquals('requireloginerror', $e->errorcode);
+        }
+
+        $this->setUser($teacher);
+        // Check warnings.
+        $groups = core_group_external::get_course_user_groups($emptycourse->id, $student1->id);
+        $groups = external_api::clean_returnvalue(core_group_external::get_course_user_groups_returns(), $groups);
+        $this->assertCount(1, $groups['warnings']);
+
+    }
+
 }
index 6e68819..d8fd647 100644 (file)
@@ -87,6 +87,7 @@ $string['configgeneralfilters'] = 'Sets the default for including filters in a b
 $string['configgeneralhistories'] = 'Sets the default for including user history within a backup.';
 $string['configgenerallogs'] = 'If enabled logs will be included in backups by default.';
 $string['configgeneralquestionbank'] = 'If enabled the question bank will be included in backups by default. PLEASE NOTE: Disabling this setting will disable the backup of activities which use the question bank, such as the quiz.';
+$string['configgeneralgroups'] = 'Sets the default for including groups and groupings in a backup.';
 $string['configgeneralroleassignments'] = 'If enabled by default roles assignments will also be backed up.';
 $string['configgeneraluserscompletion'] = 'If enabled user completion information will be included in backups by default.';
 $string['configgeneralusers'] = 'Sets the default for whether to include users in backups.';
@@ -138,6 +139,7 @@ $string['generalhistories'] = 'Include histories';
 $string['generalgradehistories'] = 'Include histories';
 $string['generallogs'] = 'Include logs';
 $string['generalquestionbank'] = 'Include question bank';
+$string['generalgroups'] = 'Include groups and groupings';
 $string['generalroleassignments'] = 'Include role assignments';
 $string['generalsettings'] = 'General backup settings';
 $string['generaluserscompletion'] = 'Include user completion information';
@@ -235,6 +237,7 @@ $string['rootsettinguserscompletion'] = 'Include user completion details';
 $string['rootsettingquestionbank'] = 'Include question bank';
 $string['rootsettinglogs'] = 'Include course logs';
 $string['rootsettinggradehistories'] = 'Include grade history';
+$string['rootsettinggroups'] = 'Include groups and groupings';
 $string['rootsettingimscc1'] = 'Convert to IMS Common Cartridge 1.0';
 $string['rootsettingimscc11'] = 'Convert to IMS Common Cartridge 1.1';
 $string['sitecourseformatwarning'] = 'This is a front page backup, note that they can only be restored on the front page';
index 21cf94e..ce03b75 100644 (file)
@@ -246,6 +246,7 @@ $string['error:requesttimeout'] = 'The connection request timed out before it co
 $string['error:requesterror'] = 'The connection request failed (error code {$a}).';
 $string['error:save'] = 'Cannot save the badge.';
 $string['error:userdeleted'] = '{$a->user} (This user no longer exists in {$a->site})';
+$string['eventbadgeawarded'] = 'Badge awarded';
 $string['evidence'] = 'Evidence';
 $string['existingrecipients'] = 'Existing badge recipients';
 $string['expired'] = 'Expired';
index 8feb0f2..2da9eee 100644 (file)
@@ -104,6 +104,7 @@ $string['periodend'] = 'until {$a}';
 $string['periodnone'] = 'enrolled {$a}';
 $string['periodstart'] = 'from {$a}';
 $string['periodstartend'] = 'from {$a->start} until {$a->end}';
+$string['proceedtocourse'] = 'Proceed to course content';
 $string['recovergrades'] = 'Recover user\'s old grades if possible';
 $string['rolefromthiscourse'] = '{$a->role} (Assigned in this course)';
 $string['rolefrommetacourse'] = '{$a->role} (Inherited from parent course)';
index 6cc2acf..95e361e 100644 (file)
@@ -34,9 +34,11 @@ $string['categoryrole'] = 'Category role';
 $string['contains'] = 'contains';
 $string['content'] = 'Content';
 $string['contentandheadings'] = 'Content and headings';
+$string['coursecategory'] = 'course category';
 $string['courserole'] = 'Course role';
 $string['courserolelabel'] = '{$a->label} is {$a->rolename} in {$a->coursename} from {$a->categoryname}';
 $string['courserolelabelerror'] = '{$a->label} error: course {$a->coursename} does not exist';
+$string['coursevalue'] = 'course value';
 $string['datelabelisafter'] = '{$a->label} is after {$a->after}';
 $string['datelabelisbefore'] = '{$a->label} is before {$a->before}';
 $string['datelabelisbetween'] = '{$a->label} is between {$a->after} and {$a->before}';
@@ -62,6 +64,7 @@ $string['isempty'] = 'is empty';
 $string['isequalto'] = 'is equal to';
 $string['isnotdefined'] = 'isn\'t defined';
 $string['isnotequalto'] = 'isn\'t equal to';
+$string['limiterfor'] = '{$a} field limiter';
 $string['neveraccessed'] = 'Never accessed';
 $string['nevermodified'] = 'Never modified';
 $string['newfilter'] = 'New filter';
@@ -69,6 +72,8 @@ $string['nofiltersenabled'] = 'No filter plugins have been enabled on this site.
 $string['off'] = 'Off';
 $string['offbutavailable'] = 'Off, but available';
 $string['on'] = 'On';
+$string['profilefilterfield'] = 'Profile field name';
+$string['profilefilterlimiter'] = 'Profile field operator';
 $string['profilelabel'] = '{$a->label}: {$a->profile} {$a->operator} {$a->value}';
 $string['profilelabelnovalue'] = '{$a->label}: {$a->profile} {$a->operator}';
 $string['removeall'] = 'Remove all filters';
@@ -78,3 +83,4 @@ $string['startswith'] = 'starts with';
 $string['tablenosave'] = 'Changes in table above are saved automatically.';
 $string['textlabel'] = '{$a->label} {$a->operator} {$a->value}';
 $string['textlabelnovalue'] = '{$a->label} {$a->operator}';
+$string['valuefor'] = '{$a} value';
index ecd7bd7..734279e 100644 (file)
@@ -32,7 +32,7 @@ $string['err_alphanumeric'] = 'You must enter only letters or numbers here.';
 $string['err_email'] = 'You must enter a valid email address here.';
 $string['err_lettersonly'] = 'You must enter only letters here.';
 $string['err_maxfiles'] = 'You must not attach more than {$a} files here.';
-$string['err_maxlength'] = 'You must enter not more than {$a->format} characters here.';
+$string['err_maxlength'] = 'You must enter no more than {$a->format} characters here.';
 $string['err_minlength'] = 'You must enter at least {$a->format} characters here.';
 $string['err_nonzero'] = 'You must enter a number not starting with a 0 here.';
 $string['err_nopunctuation'] = 'You must enter no punctuation characters here.';
index eb8c403..4b1272c 100644 (file)
@@ -375,7 +375,7 @@ $string['courserequestintro'] = 'Use this form to request a course to be created
 $string['courserequestreason'] = 'Reasons for wanting this course';
 $string['courserequestsuccess'] = 'Your course request has been saved successfully. You will be sent an email to inform you whether your request was approved.';
 $string['courserequestsupport'] = 'Supporting information to help the administrator evaluate this request';
-$string['courserequestwarning'] = 'The user requesting this course will be automatically enrolled using the "{$a}" role';
+$string['courserequestwarning'] = 'The user requesting this course will be automatically enrolled and assigned the role of {$a}.';
 $string['courserestore'] = 'Course restore';
 $string['courses'] = 'Courses';
 $string['coursesectionsummaries'] = 'Course section summaries';
@@ -1568,7 +1568,7 @@ $string['rssarticles'] = 'Number of RSS recent articles';
 $string['rsserror'] = 'Error reading RSS data';
 $string['rsserrorauth'] = 'Your RSS link does not contain a valid authentication token.';
 $string['rsserrorguest'] = 'This feed uses guest access to access the data, but guest does not have permission to read the data. Visit the original location that this feed comes from (URL) as a valid user and get a new RSS link from there.';
-$string['rsskeyshelp'] = 'To ensure security and privacy, RSS feed URLs contain a special token that identifies the user they are for. This prevents other users from accessing areas of Moodle they shouldn\'t have access to via RSS feeds.</p><p>This token is automatically created the first time you access an area of Moodle that produces an RSS feed. If you feel that your RSS feed token has been compromised in some way you can request a new one by clicking the Reset link here. Please note that your current RSS feed URLs will then become invalid.';
+$string['rsskeyshelp'] = '<p>To ensure security and privacy, RSS feed URLs contain a special token that identifies the user they are for. This prevents other users from accessing areas of the site where they are not allowed.</p><p>The token is automatically created the first time you access an area that produces an RSS feed. If you think that your RSS feed token has been compromised, you can request a new one by clicking the reset link. Please note that your current RSS feed URLs will then become invalid.</p>';
 $string['rsstype'] = 'RSS feed for this activity';
 $string['saveandnext'] = 'Save and show next';
 $string['savedat'] = 'Saved at:';
index 6e7352b..cc3eb9b 100644 (file)
@@ -61,16 +61,16 @@ $string['close'] = 'Close';
 $string['commonrepositorysettings'] = 'Common repository settings';
 $string['configallowexternallinks'] = 'This option enables all users to choose whether or not external media is copied into Moodle or not. If this is off then media is always copied into Moodle (this is usually best for overall data integrity and security).  If this is on then users can choose each time they add media to a text.';
 $string['configcacheexpire'] = 'The amount of time that file listings are cached locally (in seconds) when browsing external repositories.';
-$string['configgetfiletimeout'] = 'Timeout in seconds for downloading the external file into moodle.';
+$string['configgetfiletimeout'] = 'Timeout in seconds for downloading an external file into Moodle.';
 $string['configsaved'] = 'Configuration saved!';
 $string['configsyncfiletimeout'] = 'Timeout in seconds for syncronising the external file size.';
 $string['configsyncimagetimeout'] = 'Timeout in seconds for downloading an image file from external repository during syncronisation.';
-$string['confirmdelete'] = 'Are you sure you want to delete this repository - {$a}? If you choose "Continue and download", file references to external contents will be downloaded to moodle, but it could take long time to process.';
+$string['confirmdelete'] = 'Are you sure you want to delete the repository {$a}? If you choose "Continue and download", file references to external contents will be downloaded to Moodle. This could take a long time to process.';
 $string['confirmdeletefile'] = 'Are you sure you want to delete this file?';
 $string['confirmrenamefile'] = 'Are you sure you want to rename/move this file? There are {$a} alias/shortcut files that use this file as their source. If you proceed then those aliases will be converted to true copies.';
 $string['confirmdeletefilewithhref'] = 'Are you sure you want to delete this file? There are {$a} alias/shortcut files that use this file as their source. If you proceed then those aliases will be converted to true copies.';
 $string['confirmdeletefolder'] = 'Are you sure you want to delete this folder? All files and subfolders will be deleted.';
-$string['confirmremove'] = 'Are you sure you want to remove this repository plugin, its options and <strong>all of its instances</strong> - {$a}? If you choose "Continue and download", file references to external contents will be downloaded to moodle, but it could take long time to process.';
+$string['confirmremove'] = 'Are you sure you want to remove this repository plugin, its options and <strong>all of its instances</strong> - {$a}? If you choose "Continue and download", file references to external contents will be downloaded to Moodle. This could take a long time to process.';
 $string['confirmrenamefolder'] = ' Are you sure you want to move/rename this folder? Any alias/shortcut files that reference files in this folder will be converted into true copies.';
 $string['continueuninstall'] = 'Continue';
 $string['continueuninstallanddownload'] = 'Continue and download';
@@ -240,7 +240,7 @@ $string['usenonjsfilemanager'] = 'Open file manager in new window';
 $string['usenonjsfilepicker'] = 'Open file picker in new window';
 $string['unzipped'] = 'Unzipped successfully';
 $string['wrongcontext'] = 'You cannot access to this context';
-$string['xhtmlerror'] = 'You are probably using XHTML strict header, some YUI Component doesn\'t work in this mode, please turn it off in moodle';
+$string['xhtmlerror'] = 'You are probably using an XHTML strict header. Certain YUI components don\'t work in this mode; please turn it off.';
 $string['ziped'] = 'Compress folder successfully';
 
 // Deprecated since Moodle 2.8.
index 3bc7777..87e1007 100644 (file)
@@ -170,10 +170,10 @@ $string['defaultx'] = 'Default: {$a}';
 $string['defineroles'] = 'Define roles';
 $string['deletecourseoverrides'] = 'Delete all overrides in course';
 $string['deletelocalroles'] = 'Delete all local role assignments';
-$string['deleterolesure'] = 'Are you sure that you want to delete role "{$a->name} ({$a->shortname})"?</p><p>Currently this role is assigned to {$a->count} users.';
+$string['deleterolesure'] = '<p>Are you sure that you want to delete role "{$a->name} ({$a->shortname})"?</p><p>Currently this role is assigned to {$a->count} users.</p>';
 $string['deletexrole'] = 'Delete {$a} role';
 $string['duplicaterole'] = 'Duplicate role';
-$string['duplicaterolesure'] = 'Are you sure that you want to duplicate role "{$a->name} ({$a->shortname})"?</p>';
+$string['duplicaterolesure'] = '<p>Are you sure that you want to duplicate role "{$a->name} ({$a->shortname})"?</p>';
 $string['editingrolex'] = 'Editing role \'{$a}\'';
 $string['editrole'] = 'Edit role';
 $string['editxrole'] = 'Edit {$a} role';
index 7617216..b059127 100644 (file)
@@ -126,6 +126,7 @@ $string['missingusername'] = 'Missing username';
 $string['missingversionfile'] = 'Coding error: version.php file is missing for the component {$a}';
 $string['mobilewsdisabled'] = 'Disabled';
 $string['mobilewsenabled'] = 'Enabled';
+$string['nameexists'] = 'This name is already in use by another service';
 $string['nocapabilitytouseparameter'] = 'The user does not have the required capability to use the parameter {$a}';
 $string['nofunctions'] = 'This service has no functions.';
 $string['norequiredcapability'] = 'No required capability';
index fb679a9..1c0f8b0 100644 (file)
@@ -6447,14 +6447,17 @@ class context_user extends context {
     protected static function create_level_instances() {
         global $DB;
 
-        $sql = "INSERT INTO {context} (contextlevel, instanceid)
-                SELECT ".CONTEXT_USER.", u.id
+        $sql = "SELECT ".CONTEXT_USER.", u.id
                   FROM {user} u
                  WHERE u.deleted = 0
                        AND NOT EXISTS (SELECT 'x'
                                          FROM {context} cx
                                         WHERE u.id = cx.instanceid AND cx.contextlevel=".CONTEXT_USER.")";
-        $DB->execute($sql);
+        $contextdata = $DB->get_recordset_sql($sql);
+        foreach ($contextdata as $context) {
+            context::insert_context_record(CONTEXT_USER, $context->id, null);
+        }
+        $contextdata->close();
     }
 
     /**
@@ -6655,13 +6658,16 @@ class context_coursecat extends context {
     protected static function create_level_instances() {
         global $DB;
 
-        $sql = "INSERT INTO {context} (contextlevel, instanceid)
-                SELECT ".CONTEXT_COURSECAT.", cc.id
+        $sql = "SELECT ".CONTEXT_COURSECAT.", cc.id
                   FROM {course_categories} cc
                  WHERE NOT EXISTS (SELECT 'x'
                                      FROM {context} cx
                                     WHERE cc.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSECAT.")";
-        $DB->execute($sql);
+        $contextdata = $DB->get_recordset_sql($sql);
+        foreach ($contextdata as $context) {
+            context::insert_context_record(CONTEXT_COURSECAT, $context->id, null);
+        }
+        $contextdata->close();
     }
 
     /**
@@ -6878,13 +6884,16 @@ class context_course extends context {
     protected static function create_level_instances() {
         global $DB;
 
-        $sql = "INSERT INTO {context} (contextlevel, instanceid)
-                SELECT ".CONTEXT_COURSE.", c.id
+        $sql = "SELECT ".CONTEXT_COURSE.", c.id
                   FROM {course} c
                  WHERE NOT EXISTS (SELECT 'x'
                                      FROM {context} cx
                                     WHERE c.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSE.")";
-        $DB->execute($sql);
+        $contextdata = $DB->get_recordset_sql($sql);
+        foreach ($contextdata as $context) {
+            context::insert_context_record(CONTEXT_COURSE, $context->id, null);
+        }
+        $contextdata->close();
     }
 
     /**
@@ -7132,13 +7141,16 @@ class context_module extends context {
     protected static function create_level_instances() {
         global $DB;
 
-        $sql = "INSERT INTO {context} (contextlevel, instanceid)
-                SELECT ".CONTEXT_MODULE.", cm.id
+        $sql = "SELECT ".CONTEXT_MODULE.", cm.id
                   FROM {course_modules} cm
                  WHERE NOT EXISTS (SELECT 'x'
                                      FROM {context} cx
                                     WHERE cm.id = cx.instanceid AND cx.contextlevel=".CONTEXT_MODULE.")";
-        $DB->execute($sql);
+        $contextdata = $DB->get_recordset_sql($sql);
+        foreach ($contextdata as $context) {
+            context::insert_context_record(CONTEXT_MODULE, $context->id, null);
+        }
+        $contextdata->close();
     }
 
     /**
@@ -7349,13 +7361,16 @@ class context_block extends context {
     protected static function create_level_instances() {
         global $DB;
 
-        $sql = "INSERT INTO {context} (contextlevel, instanceid)
-                SELECT ".CONTEXT_BLOCK.", bi.id
+        $sql = "SELECT ".CONTEXT_BLOCK.", bi.id
                   FROM {block_instances} bi
                  WHERE NOT EXISTS (SELECT 'x'
                                      FROM {context} cx
                                     WHERE bi.id = cx.instanceid AND cx.contextlevel=".CONTEXT_BLOCK.")";
-        $DB->execute($sql);
+        $contextdata = $DB->get_recordset_sql($sql);
+        foreach ($contextdata as $context) {
+            context::insert_context_record(CONTEXT_BLOCK, $context->id, null);
+        }
+        $contextdata->close();
     }
 
     /**
diff --git a/lib/amd/build/first.min.js b/lib/amd/build/first.min.js
new file mode 100644 (file)
index 0000000..9dc3a61
Binary files /dev/null and b/lib/amd/build/first.min.js differ
diff --git a/lib/amd/src/first.js b/lib/amd/src/first.js
new file mode 100644 (file)
index 0000000..22188f3
--- /dev/null
@@ -0,0 +1,26 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This is an empty module, that is required before all other modules.
+ * Because every module is returned from a request for any other module, this
+ * forces the loading of all modules with a single request.
+ *
+ * @module     core/first
+ * @package    core
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(function() { });
index 80b57ce..bf5b4e1 100644 (file)
@@ -398,6 +398,15 @@ class badge {
         $result = $DB->insert_record('badge_issued', $issued, true);
 
         if ($result) {
+            // Trigger badge awarded event.
+            $eventdata = array (
+                'context' => $this->get_context(),
+                'objectid' => $this->id,
+                'relateduserid' => $userid,
+                'other' => array('dateexpire' => $issued->dateexpire, 'badgeissuedid' => $result)
+            );
+            \core\event\badge_awarded::create($eventdata)->trigger();
+
             // Lock the badge, so that its criteria could not be changed any more.
             if ($this->status == BADGE_STATUS_ACTIVE) {
                 $this->set_status(BADGE_STATUS_ACTIVE_LOCKED);
diff --git a/lib/classes/event/badge_awarded.php b/lib/classes/event/badge_awarded.php
new file mode 100644 (file)
index 0000000..4c9e021
--- /dev/null
@@ -0,0 +1,97 @@
+<?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/>.
+
+/**
+ * Badge awarded event.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      - int expiredate: Badge expire timestamp.
+ *      - int badgeissuedid: Badge issued ID.
+ * }
+ *
+ * @package    core
+ * @copyright  2015 James Ballard
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event triggered after a badge is awarded to a user.
+ *
+ * @package    core
+ * @since      Moodle 2.9
+ * @copyright  2015 James Ballard
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class badge_awarded extends base {
+
+    /**
+     * Set basic properties for the event.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'badge';
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventbadgeawarded', 'badges');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->relateduserid' has been awarded the badge with id '".$this->objectid."'.";
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/badges/overview.php', array('id' => $this->objectid));
+    }
+
+    /**
+     * Custom validations.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('The \'objectid\' must be set.');
+        }
+    }
+}
diff --git a/lib/classes/requirejs.php b/lib/classes/requirejs.php
new file mode 100644 (file)
index 0000000..9894dc5
--- /dev/null
@@ -0,0 +1,130 @@
+<?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/>.
+
+/**
+ * RequireJS helper functions.
+ *
+ * @package    core
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Collection of requirejs related methods.
+ *
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_requirejs {
+
+    /**
+     * Check a single module exists and return the full path to it.
+     *
+     * The expected location for amd modules is:
+     *  <componentdir>/amd/src/modulename.js
+     *
+     * @param string $component The component determines the folder the js file should be in.
+     * @param string $jsfilename The filename for the module (with the js extension).
+     * @param boolean $debug If true, returns the paths to the original (unminified) source files.
+     * @return array $files An array of mappings from module names to file paths.
+     *                      Empty array if the file does not exist.
+     */
+    public static function find_one_amd_module($component, $jsfilename, $debug = false) {
+        $jsfileroot = core_component::get_component_directory($component);
+        if (!$jsfileroot) {
+            return array();
+        }
+
+        $module = str_replace('.js', '', $jsfilename);
+
+        $srcdir = $jsfileroot . '/amd/build';
+        $minpart = '.min';
+        if ($debug) {
+            $srcdir = $jsfileroot . '/amd/src';
+            $minpart = '';
+        }
+
+        $filename = $srcdir . '/' . $module . $minpart . '.js';
+        if (!file_exists($filename)) {
+            return array();
+        }
+
+        $fullmodulename = $component . '/' . $module;
+        return array($fullmodulename => $filename);
+    }
+
+    /**
+     * Scan the source for AMD modules and return them all.
+     *
+     * The expected location for amd modules is:
+     *  <componentdir>/amd/src/modulename.js
+     *
+     * @param boolean $debug If true, returns the paths to the original (unminified) source files.
+     * @return array $files An array of mappings from module names to file paths.
+     */
+    public static function find_all_amd_modules($debug = false) {
+        global $CFG;
+
+        $jsdirs = array();
+        $jsfiles = array();
+
+        $dir = $CFG->libdir . '/amd';
+        if (!empty($dir) && is_dir($dir)) {
+            $jsdirs['core'] = $dir;
+        }
+        $subsystems = core_component::get_core_subsystems();
+        foreach ($subsystems as $subsystem => $dir) {
+            if (!empty($dir) && is_dir($dir . '/amd')) {
+                $jsdirs[$subsystem] = $dir . '/amd';
+            }
+        }
+        $plugintypes = core_component::get_plugin_types();
+        foreach ($plugintypes as $type => $dir) {
+            $plugins = core_component::get_plugin_list_with_file($type, 'amd', false);
+            foreach ($plugins as $plugin => $dir) {
+                if (!empty($dir) && is_dir($dir)) {
+                    $jsdirs[$type . '_' . $plugin] = $dir;
+                }
+            }
+        }
+
+        foreach ($jsdirs as $component => $dir) {
+            $srcdir = $dir . '/build';
+            if ($debug) {
+                $srcdir = $dir . '/src';
+            }
+            $items = new RecursiveDirectoryIterator($srcdir);
+            foreach ($items as $item) {
+                $extension = $item->getExtension();
+                if ($extension === 'js') {
+                    $filename = str_replace('.min', '', $item->getBaseName('.js'));
+                    // We skip lazy loaded modules.
+                    if (strpos($filename, '-lazy') === false) {
+                        $modulename = $component . '/' . $filename;
+                        $jsfiles[$modulename] = $item->getRealPath();
+                    }
+                }
+                unset($item);
+            }
+            unset($items);
+        }
+
+        return $jsfiles;
+    }
+
+}
index 972a30f..cac0f6e 100644 (file)
@@ -323,8 +323,11 @@ class component_installer {
     /// Move current revision to a safe place
         $destinationdir = $CFG->dataroot.'/'.$this->destpath;
         $destinationcomponent = $destinationdir.'/'.$this->componentname;
-        @remove_dir($destinationcomponent.'_old');     //Deleting possible old components before
-        @rename ($destinationcomponent, $destinationcomponent.'_old');  //Moving to a safe place
+        @remove_dir($destinationcomponent.'_old');     // Deleting a possible old version.
+
+        // Moving to a safe place.
+        @rename($destinationcomponent, $destinationcomponent.'_old');
+
     /// Unzip new version
         if (!unzip_file($zipfile, $destinationdir, false)) {
         /// Error so, go back to the older
index 5815826..a6391b9 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20150211" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20150224" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
       <INDEXES>
         <INDEX NAME="useridfrom" UNIQUE="false" FIELDS="useridfrom"/>
         <INDEX NAME="useridto" UNIQUE="false" FIELDS="useridto"/>
+        <INDEX NAME="useridfromto" UNIQUE="false" FIELDS="useridfrom, useridto"/>
       </INDEXES>
     </TABLE>
     <TABLE NAME="message_read" COMMENT="Stores all messages that have been read">
       <INDEXES>
         <INDEX NAME="useridfrom" UNIQUE="false" FIELDS="useridfrom"/>
         <INDEX NAME="useridto" UNIQUE="false" FIELDS="useridto"/>
+        <INDEX NAME="useridfromto" UNIQUE="false" FIELDS="useridfrom, useridto"/>
       </INDEXES>
     </TABLE>
     <TABLE NAME="message_contacts" COMMENT="Maintains lists of relationships between users">
         <KEY NAME="pushid-userid" TYPE="unique" FIELDS="pushid, userid"/>
         <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
       </KEYS>
+      <INDEXES>
+        <INDEX NAME="uuid-userid" UNIQUE="false" FIELDS="uuid, userid" COMMENT="Index on uuid and userid"/>
+      </INDEXES>
     </TABLE>
     <TABLE NAME="user_password_resets" COMMENT="table tracking password reset confirmation tokens">
       <FIELDS>
index 9aa27c1..1b60c7a 100644 (file)
@@ -303,6 +303,15 @@ $functions = array(
         'type'        => 'write',
     ),
 
+    'core_group_get_course_user_groups' => array(
+        'classname'     => 'core_group_external',
+        'methodname'    => 'get_course_user_groups',
+        'classpath'     => 'group/externallib.php',
+        'description'   => 'Returns all groups in specified course for the specified user.',
+        'type'          => 'read',
+        'capabilities'  => 'moodle/course:managegroups',
+    ),
+
     // === file related functions ===
 
     'moodle_file_get_files' => array(
@@ -465,6 +474,15 @@ $functions = array(
         'capabilities'=> '',
     ),
 
+    'core_user_remove_user_device' => array(
+        'classname'     => 'core_user_external',
+        'methodname'    => 'remove_user_device',
+        'classpath'     => 'user/externallib.php',
+        'description'   => 'Remove a user device from the Moodle database.',
+        'type'          => 'write',
+        'capabilities'  => '',
+    ),
+
     // === enrol related functions ===
 
     'core_enrol_get_enrolled_users_with_capability' => array(
@@ -985,7 +1003,9 @@ $services = array(
             'core_message_get_contacts',
             'core_message_search_contacts',
             'core_message_get_blocked_users',
-            'gradereport_user_get_grades_table'
+            'gradereport_user_get_grades_table',
+            'core_group_get_course_user_groups',
+            'core_user_remove_user_device',
             ),
         'enabled' => 0,
         'restrictedusers' => 0,
index 3526517..8dc4f4e 100644 (file)
@@ -125,8 +125,8 @@ $tasks = array(
     array(
         'classname' => 'core\task\create_contexts_task',
         'blocking' => 1,
-        'minute' => '*',
-        'hour' => '*',
+        'minute' => '0',
+        'hour' => '0',
         'day' => '*',
         'dayofweek' => '*',
         'month' => '*'
index 2b03cf6..06be814 100644 (file)
@@ -4177,5 +4177,38 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2015021100.00);
     }
 
+    if ($oldversion < 2015022401.00) {
+
+        // Define index useridfromto (not unique) to be added to message.
+        $table = new xmldb_table('message');
+        $index = new xmldb_index('useridfromto', XMLDB_INDEX_NOTUNIQUE, array('useridfrom', 'useridto'));
+
+        // Conditionally launch add index useridfromto.
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Define index useridfromto (not unique) to be added to message_read.
+        $table = new xmldb_table('message_read');
+        $index = new xmldb_index('useridfromto', XMLDB_INDEX_NOTUNIQUE, array('useridfrom', 'useridto'));
+
+        // Conditionally launch add index useridfromto.
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2015022401.00);
+    }
+
+    if ($oldversion < 2015022500.00) {
+        $table = new xmldb_table('user_devices');
+        $index = new xmldb_index('uuid-userid', XMLDB_INDEX_NOTUNIQUE, array('uuid', 'userid'));
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+        upgrade_main_savepoint(true, 2015022500.00);
+    }
+
     return true;
 }
index b346311..f51facd 100644 (file)
@@ -656,6 +656,7 @@ class zip_archive extends file_archive {
                             case 'ISO-8859-6': $encoding = 'CP720'; break;
                             case 'ISO-8859-7': $encoding = 'CP737'; break;
                             case 'ISO-8859-8': $encoding = 'CP862'; break;
+                            case 'EUC-JP':
                             case 'UTF-8':
                                 if ($winchar = get_string('localewincharset', 'langconfig')) {
                                     // Most probably works only for zh_cn,
index 431f0c7..46bcf4c 100644 (file)
@@ -767,6 +767,8 @@ class grade_category extends grade_object {
      * Set the flags on the grade_grade items to indicate how individual grades are used
      * in the aggregation.
      *
+     * WARNING: This function is called a lot during gradebook recalculation, be very performance considerate.
+     *
      * @param int $userid The user we have aggregated the grades for.
      * @param array $usedweights An array with keys for each of the grade_item columns included in the aggregation. The value are the relative weight.
      * @param array $novalue An array with keys for each of the grade_item columns skipped because
@@ -779,44 +781,32 @@ class grade_category extends grade_object {
     private function set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit) {
         global $DB;
 
-        // First set them all to weight null and status = 'unknown'.
-        if ($allitems = grade_item::fetch_all(array('categoryid'=>$this->id))) {
-            list($itemsql, $itemlist) = $DB->get_in_or_equal(array_keys($allitems), SQL_PARAMS_NAMED, 'g');
+        // Reset aggregation to unknown and 0 for all grade items for this user and category.
+        $params = array('categoryid' => $this->id, 'userid' => $userid);
+        $itemssql = "SELECT id
+                       FROM {grade_items}
+                      WHERE categoryid = :categoryid";
 
-            $itemlist['userid'] = $userid;
+        $sql = "UPDATE {grade_grades}
+                   SET aggregationstatus = 'unknown',
+                       aggregationweight = 0
+                 WHERE userid = :userid
+                   AND itemid IN ($itemssql)";
 
-            $DB->set_field_select('grade_grades',
-                                  'aggregationstatus',
-                                  'unknown',
-                                  "itemid $itemsql AND userid = :userid",
-                                  $itemlist);
-            $DB->set_field_select('grade_grades',
-                                  'aggregationweight',
-                                  0,
-                                  "itemid $itemsql AND userid = :userid",
-                                  $itemlist);
-        }
+        $DB->execute($sql, $params);
 
-        // Included.
+        // Included with weights.
         if (!empty($usedweights)) {
             // The usedweights items are updated individually to record the weights.
             foreach ($usedweights as $gradeitemid => $contribution) {
-                $DB->set_field_select('grade_grades',
-                                      'aggregationweight',
-                                      $contribution,
-                                      "itemid = :itemid AND userid = :userid",
-                                      array('itemid'=>$gradeitemid, 'userid'=>$userid));
-            }
+                $sql = "UPDATE {grade_grades}
+                           SET aggregationstatus = 'used',
+                               aggregationweight = :contribution
+                         WHERE itemid = :itemid AND userid = :userid";
 
-            // Now set the status flag for all these weights.
-            list($itemsql, $itemlist) = $DB->get_in_or_equal(array_keys($usedweights), SQL_PARAMS_NAMED, 'g');
-            $itemlist['userid'] = $userid;
-
-            $DB->set_field_select('grade_grades',
-                                  'aggregationstatus',
-                                  'used',
-                                  "itemid $itemsql AND userid = :userid",
-                                  $itemlist);
+                $params = array('contribution' => $contribution, 'itemid' => $gradeitemid, 'userid' => $userid);
+                $DB->execute($sql, $params);
+            }
         }
 
         // No value.
@@ -844,6 +834,7 @@ class grade_category extends grade_object {
                                   "itemid $itemsql AND userid = :userid",
                                   $itemlist);
         }
+
         // Extra credit.
         if (!empty($extracredit)) {
             list($itemsql, $itemlist) = $DB->get_in_or_equal(array_keys($extracredit), SQL_PARAMS_NAMED, 'g');
index b3e1726..8b6af7c 100644 (file)
@@ -597,14 +597,32 @@ M.util.init_block_hider = function(Y, config) {
                     this.set('block', '#'+this.get('id'));
                     var b = this.get('block'),
                         t = b.one('.title'),
-                        a = null;
+                        a = null,
+                        hide,
+                        show;
                     if (t && (a = t.one('.block_action'))) {
-                        var hide = Y.Node.create('<img class="block-hider-hide" tabindex="0" alt="'+config.tooltipVisible+'" title="'+config.tooltipVisible+'" />');
-                        hide.setAttribute('src', this.get('iconVisible')).on('click', this.updateState, this, true);
+                        hide = Y.Node.create('<img />')
+                            .addClass('block-hider-hide')
+                            .setAttrs({
+                                alt:        config.tooltipVisible,
+                                src:        this.get('iconVisible'),
+                                tabindex:   0,
+                                'title':    config.tooltipVisible
+                            });
                         hide.on('keypress', this.updateStateKey, this, true);
-                        var show = Y.Node.create('<img class="block-hider-show" tabindex="0" alt="'+config.tooltipHidden+'" title="'+config.tooltipHidden+'" />');
-                        show.setAttribute('src', this.get('iconHidden')).on('click', this.updateState, this, false);
+                        hide.on('click', this.updateState, this, true);
+
+                        show = Y.Node.create('<img />')
+                            .addClass('block-hider-show')
+                            .setAttrs({
+                                alt:        config.tooltipHidden,
+                                src:        this.get('iconHidden'),
+                                tabindex:   0,
+                                'title':    config.tooltipHidden
+                            });
                         show.on('keypress', this.updateStateKey, this, false);
+                        show.on('click', this.updateState, this, false);
+
                         a.insert(show, 0).insert(hide, 0);
                     }
                 },
index 7b64921..2747982 100644 (file)
@@ -2853,10 +2853,6 @@ function require_login($courseorid = null, $autologinguest = true, $cm = null, $
                 $modinfo = get_fast_modinfo($course);
                 $cm = $modinfo->get_cm($cm->id);
             }
-            $PAGE->set_cm($cm, $course); // Set's up global $COURSE.
-            $PAGE->set_pagelayout('incourse');
-        } else {
-            $PAGE->set_course($course); // Set's up global $COURSE.
         }
     } else {
         // Do not touch global $COURSE via $PAGE->set_course(),
@@ -2960,6 +2956,13 @@ function require_login($courseorid = null, $autologinguest = true, $cm = null, $
 
     // Do not bother admins with any formalities.
     if (is_siteadmin()) {
+        // Set the global $COURSE.
+        if ($cm) {
+            $PAGE->set_cm($cm, $course);
+            $PAGE->set_pagelayout('incourse');
+        } else if (!empty($courseorid)) {
+            $PAGE->set_course($course);
+        }
         // Set accesstime or the user will appear offline which messes up messaging.
         user_accesstime_log($course->id);
         return;
@@ -3017,6 +3020,7 @@ function require_login($courseorid = null, $autologinguest = true, $cm = null, $
                 if ($preventredirect) {
                     throw new require_login_exception('Course is hidden');
                 }
+                $PAGE->set_context(null);
                 // We need to override the navigation URL as the course won't have been added to the navigation and thus
                 // the navigation will mess up when trying to find it.
                 navigation_node::override_active_url(new moodle_url('/'));
@@ -3037,6 +3041,7 @@ function require_login($courseorid = null, $autologinguest = true, $cm = null, $
                 if ($preventredirect) {
                     throw new require_login_exception('Invalid course login-as access');
                 }
+                $PAGE->set_context(null);
                 echo $OUTPUT->header();
                 notice(get_string('studentnotallowed', '', fullname($USER, true)), $CFG->wwwroot .'/');
             }
@@ -3151,6 +3156,14 @@ function require_login($courseorid = null, $autologinguest = true, $cm = null, $
         redirect($url, get_string('activityiscurrentlyhidden'));
     }
 
+    // Set the global $COURSE.
+    if ($cm) {
+        $PAGE->set_cm($cm, $course);
+        $PAGE->set_pagelayout('incourse');
+    } else if (!empty($courseorid)) {
+        $PAGE->set_course($course);
+    }
+
     // Finally access granted, update lastaccess times.
     user_accesstime_log($course->id);
 }
index 46c2da2..31c4d0d 100644 (file)
@@ -35,11 +35,12 @@ defined('MOODLE_INTERNAL') || die();
  *
  * Typical usage would be
  * <pre>
- *     $PAGE->requires->js_init_call('M.mod_forum.init_view');
+ *     $PAGE->requires->js_call_amd('mod_forum/view', 'init');
  * </pre>
  *
- * It also supports obsoleted coding style withouth YUI3 modules.
+ * It also supports obsoleted coding style with/without YUI3 modules.
  * <pre>
+ *     $PAGE->requires->js_init_call('M.mod_forum.init_view');
  *     $PAGE->requires->css('/mod/mymod/userstyles.php?id='.$id); // not overridable via themes!
  *     $PAGE->requires->js('/mod/mymod/script.js');
  *     $PAGE->requires->js('/mod/mymod/small_but_urgent.js', true);
@@ -78,6 +79,11 @@ class page_requirements_manager {
      */
     protected $jsincludes = array('head'=>array(), 'footer'=>array());
 
+    /**
+     * @var array Inline scripts using RequireJS module loading.
+     */
+    protected $amdjscode = array('');
+
     /**
      * @var array List of needed function calls
      */
@@ -943,6 +949,52 @@ class page_requirements_manager {
         $this->jscalls[$where][] = array($function, $arguments, $delay);
     }
 
+    /**
+     * This function appends a block of code to the AMD specific javascript block executed
+     * in the page footer, just after loading the requirejs library.
+     *
+     * The code passed here can rely on AMD module loading, e.g. require('jquery', function($) {...});
+     *
+     * @param string $code The JS code to append.
+     */
+    public function js_amd_inline($code) {
+        $this->amdjscode[] = $code;
+    }
+
+    /**
+     * This function creates a minimal JS script that requires and calls a single function from an AMD module with arguments.
+     * If it is called multiple times, it will be executed multiple times.
+     *
+     * @param string $fullmodule The format for module names is <component name>/<module name>.
+     * @param string $func The function from the module to call
+     * @param array $params The params to pass to the function. They will be json encoded, so no nasty classes/types please.
+     */
+    public function js_call_amd($fullmodule, $func, $params = array()) {
+        global $CFG;
+
+        list($component, $module) = explode('/', $fullmodule, 2);
+
+        $component = clean_param($component, PARAM_COMPONENT);
+        $module = clean_param($module, PARAM_ALPHANUMEXT);
+        $func = clean_param($func, PARAM_ALPHANUMEXT);
+
+        $jsonparams = array();
+        foreach ($params as $param) {
+            $jsonparams[] = json_encode($param);
+        }
+        $strparams = implode(', ', $jsonparams);
+        if ($CFG->debugdeveloper) {
+            $toomanyparamslimit = 1024;
+            if (strlen($strparams) > $toomanyparamslimit) {
+                debugging('Too many params passed to js_call_amd("' . $fullmodule . '", "' . $func . '")', DEBUG_DEVELOPER);
+            }
+        }
+
+        $js = 'require(["' . $component . '/' . $module . '"], function(amd) { amd.' . $func . '(' . $strparams . '); });';
+
+        $this->js_amd_inline($js);
+    }
+
     /**
      * Creates a JavaScript function call that requires one or more modules to be loaded.
      *
@@ -952,6 +1004,9 @@ class page_requirements_manager {
      *     - Moodle modules  [moodle-*]
      *     - Gallery modules [gallery-*]
      *
+     * Before writing new code that makes extensive use of YUI, you should consider it's replacement AMD/JQuery.
+     * @see js_call_amd()
+     *
      * @param array|string $modules One or more modules
      * @param string $function The function to call once modules have been loaded
      * @param array $arguments An array of arguments to pass to the function
@@ -1205,6 +1260,47 @@ class page_requirements_manager {
         return '';
     }
 
+    /**
+     * Returns js code to load amd module loader, then insert inline script tags
+     * that contain require() calls using RequireJS.
+     * @return string
+     */
+    protected function get_amd_footercode() {
+        global $CFG;
+        $output = '';
+        $jsrev = $this->get_jsrev();
+
+        $jsloader = new moodle_url($CFG->httpswwwroot . '/lib/javascript.php');
+        $jsloader->set_slashargument('/' . $jsrev . '/');
+        $requirejsloader = new moodle_url($CFG->httpswwwroot . '/lib/requirejs.php');
+        $requirejsloader->set_slashargument('/' . $jsrev . '/');
+
+        $requirejsconfig = file_get_contents($CFG->dirroot . '/lib/requirejs/moodle-config.js');
+
+        // No extension required unless slash args is disabled.
+        $jsextension = '.js';
+        if (!empty($CFG->slasharguments)) {
+            $jsextension = '';
+        }
+
+        $requirejsconfig = str_replace('[BASEURL]', $requirejsloader, $requirejsconfig);
+        $requirejsconfig = str_replace('[JSURL]', $jsloader, $requirejsconfig);
+        $requirejsconfig = str_replace('[JSEXT]', $jsextension, $requirejsconfig);
+
+        $output .= html_writer::script($requirejsconfig);
+        if ($CFG->debugdeveloper) {
+            $output .= html_writer::script('', $this->js_fix_url('/lib/requirejs/require.js'));
+        } else {
+            $output .= html_writer::script('', $this->js_fix_url('/lib/requirejs/require.min.js'));
+        }
+
+        // First include must be to a module with no dependencies, this prevents multiple requests.
+        $prefix = "require(['core/first'], function() {\n";
+        $suffix = "\n});";
+        $output .= html_writer::script($prefix . implode(";\n", $this->amdjscode) . $suffix);
+        return $output;
+    }
+
     /**
      * Returns basic YUI3 JS loading code.
      * YUI3 is using autoloading of both CSS and JS code.
@@ -1426,9 +1522,13 @@ class page_requirements_manager {
      */
     public function get_end_code() {
         global $CFG;
+        $output = '';
+
+        // Call amd init functions.
+        $output .= $this->get_amd_footercode();
 
         // Add other requested modules.
-        $output = $this->get_extra_modules_code();
+        $output .= $this->get_extra_modules_code();
 
         $this->js_init_code('M.util.js_complete("init");', true);
 
diff --git a/lib/requirejs.php b/lib/requirejs.php
new file mode 100644 (file)
index 0000000..7cc8d0f
--- /dev/null
@@ -0,0 +1,144 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file is serving optimised JS for RequireJS.
+ *
+ * @package    core
+ * @copyright  2015 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// Disable moodle specific debug messages and any errors in output,
+// comment out when debugging or better look into error log!
+define('NO_DEBUG_DISPLAY', true);
+
+// We need just the values from config.php and minlib.php.
+define('ABORT_AFTER_CONFIG', true);
+require('../config.php'); // This stops immediately at the beginning of lib/setup.php.
+require_once("$CFG->dirroot/lib/jslib.php");
+require_once("$CFG->dirroot/lib/classes/requirejs.php");
+
+$slashargument = min_get_slash_argument();
+if (!$slashargument) {
+    // The above call to min_get_slash_argument should always work.
+    die('Invalid request');
+}
+
+$slashargument = ltrim($slashargument, '/');
+if (substr_count($slashargument, '/') < 1) {
+    header('HTTP/1.0 404 not found');
+    die('Slash argument must contain both a revision and a file path');
+}
+// Split into revision and module name.
+list($rev, $file) = explode('/', $slashargument, 2);
+$rev  = min_clean_param($rev, 'INT');
+$file = '/' . min_clean_param($file, 'SAFEPATH');
+
+// Only load js files from the js modules folder from the components.
+$jsfiles = array();
+list($unused, $component, $module) = explode('/', $file, 3);
+
+// No subdirs allowed - only flat module structure please.
+if (strpos('/', $module) !== false) {
+    die('Invalid module');
+}
+
+// Some (huge) modules are better loaded lazily (when they are used). If we are requesting
+// one of these modules, only return the one module, not the combo.
+$lazysuffix = "-lazy.js";
+$lazyload = (strpos($module, $lazysuffix) !== false);
+
+if ($lazyload) {
+    // We are lazy loading a single file - so include the component/filename pair in the etag.
+    $etag = sha1($rev . '/' . $component . '/' . $module);
+} else {
+    // We loading all (non-lazy) files - so only the rev makes this request unique.
+    $etag = sha1($rev);
+}
+
+
+// Use the caching only for meaningful revision numbers which prevents future cache poisoning.
+if ($rev > 0 and $rev < (time() + 60 * 60)) {
+    $candidate = $CFG->localcachedir . '/requirejs/' . $etag;
+
+    if (file_exists($candidate)) {
+        if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
+            // We do not actually need to verify the etag value because our files
+            // never change in cache because we increment the rev parameter.
+            js_send_unmodified(filemtime($candidate), $etag);
+        }
+        js_send_cached($candidate, $etag, 'requirejs.php');
+        exit(0);
+
+    } else {
+        $jsfiles = array();
+        if ($lazyload) {
+            $jsfiles = core_requirejs::find_one_amd_module($component, $module);
+        } else {
+            // Here we respond to the request by returning ALL amd modules. This saves
+            // round trips in production.
+
+            $jsfiles = core_requirejs::find_all_amd_modules();
+        }
+
+        $content = '';
+        foreach ($jsfiles as $modulename => $jsfile) {
+            $js = file_get_contents($jsfile) . "\n";
+            // Inject the module name into the define.
+            $replace = 'define(\'' . $modulename . '\', ';
+            $search = 'define(';
+            // Replace only the first occurrence.
+            $js = implode($replace, explode($search, $js, 2));
+            $content .= $js;
+        }
+
+        js_write_cache_file_content($candidate, $content);
+        // Verify nothing failed in cache file creation.
+        clearstatcache();
+        if (file_exists($candidate)) {
+            js_send_cached($candidate, $etag, 'requirejs.php');
+            exit(0);
+        }
+    }
+}
+
+if ($lazyload) {
+    $jsfiles = core_requirejs::find_one_amd_module($component, $module, true);
+} else {
+    $jsfiles = core_requirejs::find_all_amd_modules(true);
+}
+
+$content = '';
+foreach ($jsfiles as $modulename => $jsfile) {
+    $shortfilename = str_replace($CFG->dirroot, '', $jsfile);
+    $js = "// ---- $shortfilename ----\n";
+    $js .= file_get_contents($jsfile) . "\n";
+    // Inject the module name into the define.
+    $replace = 'define(\'' . $modulename . '\', ';
+    $search = 'define(';
+
+    if (strpos($js, $search) === false) {
+        // We can't call debugging because we only have minimal config loaded.
+        header('HTTP/1.0 500 error');
+        die('JS file: ' . $shortfilename . ' does not contain a javascript module in AMD format. "define()" not found.');
+    }
+
+    // Replace only the first occurrence.
+    $js = implode($replace, explode($search, $js, 2));
+    $content .= $js;
+}
+js_send_uncached($content, $etag, 'requirejs.php');
diff --git a/lib/requirejs/LICENSE b/lib/requirejs/LICENSE
new file mode 100644 (file)
index 0000000..d3b4181
--- /dev/null
@@ -0,0 +1,58 @@
+RequireJS is released under two licenses: new BSD, and MIT. You may pick the
+license that best suits your development needs. The text of both licenses are
+provided below.
+
+
+The "New" BSD License:
+----------------------
+
+Copyright (c) 2010-2014, The Dojo Foundation
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+  * Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+  * Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation
+    and/or other materials provided with the distribution.
+  * Neither the name of the Dojo Foundation nor the names of its contributors
+    may be used to endorse or promote products derived from this software
+    without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+
+MIT License
+-----------
+
+Copyright (c) 2010-2014, The Dojo Foundation
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/lib/requirejs/jquery-private.js b/lib/requirejs/jquery-private.js
new file mode 100644 (file)
index 0000000..eaac228
--- /dev/null
@@ -0,0 +1,28 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This module depends on the real jquery - and returns the non-global version of it.
+ *
+ * @module     jquery-private
+ * @package    core
+ * @copyright  2015 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery'], function ($) {
+    // This noConflict call tells JQuery to remove the variable from the global scope - so
+    // the only remaining instance will be the sandboxed one.
+    return $.noConflict( true );
+});
diff --git a/lib/requirejs/moodle-config.js b/lib/requirejs/moodle-config.js
new file mode 100644 (file)
index 0000000..06c216d
--- /dev/null
@@ -0,0 +1,24 @@
+var require = {
+    baseUrl : '[BASEURL]',
+    // We only support AMD modules with an explicit define() statement.
+    enforceDefine: true,
+    skipDataMain: true,
+
+    paths: {
+        jquery: '[JSURL]lib/jquery/jquery-1.11.1.min[JSEXT]',
+        jqueryui: '[JSURL]lib/jquery/ui-1.11.1/jquery-ui.min[JSEXT]',
+        jqueryprivate: '[JSURL]lib/requirejs/jquery-private[JSEXT]'
+    },
+
+    // Custom jquery config map.
+    map: {
+      // '*' means all modules will get 'jqueryprivate'
+      // for their 'jquery' dependency.
+      '*': { jquery: 'jqueryprivate' },
+
+      // 'jquery-private' wants the real jQuery module
+      // though. If this line was not here, there would
+      // be an unresolvable cyclic dependency.
+      jqueryprivate: { jquery: 'jquery' }
+    }
+};
diff --git a/lib/requirejs/require.js b/lib/requirejs/require.js
new file mode 100644 (file)
index 0000000..77a5bb1
--- /dev/null
@@ -0,0 +1,2076 @@
+/** vim: et:ts=4:sw=4:sts=4
+ * @license RequireJS 2.1.15 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved.
+ * Available via the MIT or new BSD license.
+ * see: http://github.com/jrburke/requirejs for details
+ */
+//Not using strict: uneven strict support in browsers, #392, and causes
+//problems with requirejs.exec()/transpiler plugins that may not be strict.
+/*jslint regexp: true, nomen: true, sloppy: true */
+/*global window, navigator, document, importScripts, setTimeout, opera */
+
+var requirejs, require, define;
+(function (global) {
+    var req, s, head, baseElement, dataMain, src,
+        interactiveScript, currentlyAddingScript, mainScript, subPath,
+        version = '2.1.15',
+        commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,
+        cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,
+        jsSuffixRegExp = /\.js$/,
+        currDirRegExp = /^\.\//,
+        op = Object.prototype,
+        ostring = op.toString,
+        hasOwn = op.hasOwnProperty,
+        ap = Array.prototype,
+        apsp = ap.splice,
+        isBrowser = !!(typeof window !== 'undefined' && typeof navigator !== 'undefined' && window.document),
+        isWebWorker = !isBrowser && typeof importScripts !== 'undefined',
+        //PS3 indicates loaded and complete, but need to wait for complete
+        //specifically. Sequence is 'loading', 'loaded', execution,
+        // then 'complete'. The UA check is unfortunate, but not sure how
+        //to feature test w/o causing perf issues.
+        readyRegExp = isBrowser && navigator.platform === 'PLAYSTATION 3' ?
+                      /^complete$/ : /^(complete|loaded)$/,
+        defContextName = '_',
+        //Oh the tragedy, detecting opera. See the usage of isOpera for reason.
+        isOpera = typeof opera !== 'undefined' && opera.toString() === '[object Opera]',
+        contexts = {},
+        cfg = {},
+        globalDefQueue = [],
+        useInteractive = false;
+
+    function isFunction(it) {
+        return ostring.call(it) === '[object Function]';
+    }
+
+    function isArray(it) {
+        return ostring.call(it) === '[object Array]';
+    }
+
+    /**
+     * Helper function for iterating over an array. If the func returns
+     * a true value, it will break out of the loop.
+     */
+    function each(ary, func) {
+        if (ary) {
+            var i;
+            for (i = 0; i < ary.length; i += 1) {
+                if (ary[i] && func(ary[i], i, ary)) {
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+     * Helper function for iterating over an array backwards. If the func
+     * returns a true value, it will break out of the loop.
+     */
+    function eachReverse(ary, func) {
+        if (ary) {
+            var i;
+            for (i = ary.length - 1; i > -1; i -= 1) {
+                if (ary[i] && func(ary[i], i, ary)) {
+                    break;
+                }
+            }
+        }
+    }
+
+    function hasProp(obj, prop) {
+        return hasOwn.call(obj, prop);
+    }
+
+    function getOwn(obj, prop) {
+        return hasProp(obj, prop) && obj[prop];
+    }
+
+    /**
+     * Cycles over properties in an object and calls a function for each
+     * property value. If the function returns a truthy value, then the
+     * iteration is stopped.
+     */
+    function eachProp(obj, func) {
+        var prop;
+        for (prop in obj) {
+            if (hasProp(obj, prop)) {
+                if (func(obj[prop], prop)) {
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+     * Simple function to mix in properties from source into target,
+     * but only if target does not already have a property of the same name.
+     */
+    function mixin(target, source, force, deepStringMixin) {
+        if (source) {
+            eachProp(source, function (value, prop) {
+                if (force || !hasProp(target, prop)) {
+                    if (deepStringMixin && typeof value === 'object' && value &&
+                        !isArray(value) && !isFunction(value) &&
+                        !(value instanceof RegExp)) {
+
+                        if (!target[prop]) {
+                            target[prop] = {};
+                        }
+                        mixin(target[prop], value, force, deepStringMixin);
+                    } else {
+                        target[prop] = value;
+                    }
+                }
+            });
+        }
+        return target;
+    }
+
+    //Similar to Function.prototype.bind, but the 'this' object is specified
+    //first, since it is easier to read/figure out what 'this' will be.
+    function bind(obj, fn) {
+        return function () {
+            return fn.apply(obj, arguments);
+        };
+    }
+
+    function scripts() {
+        return document.getElementsByTagName('script');
+    }
+
+    function defaultOnError(err) {
+        throw err;
+    }
+
+    //Allow getting a global that is expressed in
+    //dot notation, like 'a.b.c'.
+    function getGlobal(value) {
+        if (!value) {
+            return value;
+        }
+        var g = global;
+        each(value.split('.'), function (part) {
+            g = g[part];
+        });
+        return g;
+    }
+
+    /**
+     * Constructs an error with a pointer to an URL with more information.
+     * @param {String} id the error ID that maps to an ID on a web page.
+     * @param {String} message human readable error.
+     * @param {Error} [err] the original error, if there is one.
+     *
+     * @returns {Error}
+     */
+    function makeError(id, msg, err, requireModules) {
+        var e = new Error(msg + '\nhttp://requirejs.org/docs/errors.html#' + id);
+        e.requireType = id;
+        e.requireModules = requireModules;
+        if (err) {
+            e.originalError = err;
+        }
+        return e;
+    }
+
+    if (typeof define !== 'undefined') {
+        //If a define is already in play via another AMD loader,
+        //do not overwrite.
+        return;
+    }
+
+    if (typeof requirejs !== 'undefined') {
+        if (isFunction(requirejs)) {
+            //Do not overwrite an existing requirejs instance.
+            return;
+        }
+        cfg = requirejs;
+        requirejs = undefined;
+    }
+
+    //Allow for a require config object
+    if (typeof require !== 'undefined' && !isFunction(require)) {
+        //assume it is a config object.
+        cfg = require;
+        require = undefined;
+    }
+
+    function newContext(contextName) {
+        var inCheckLoaded, Module, context, handlers,
+            checkLoadedTimeoutId,
+            config = {
+                //Defaults. Do not set a default for map
+                //config to speed up normalize(), which
+                //will run faster if there is no default.
+                waitSeconds: 7,
+                baseUrl: './',
+                paths: {},
+                bundles: {},
+                pkgs: {},
+                shim: {},
+                config: {}
+            },
+            registry = {},
+            //registry of just enabled modules, to speed
+            //cycle breaking code when lots of modules
+            //are registered, but not activated.
+            enabledRegistry = {},
+            undefEvents = {},
+            defQueue = [],
+            defined = {},
+            urlFetched = {},
+            bundlesMap = {},
+            requireCounter = 1,
+            unnormalizedCounter = 1;
+
+        /**
+         * Trims the . and .. from an array of path segments.
+         * It will keep a leading path segment if a .. will become
+         * the first path segment, to help with module name lookups,
+         * which act like paths, but can be remapped. But the end result,
+         * all paths that use this function should look normalized.
+         * NOTE: this method MODIFIES the input array.
+         * @param {Array} ary the array of path segments.
+         */
+        function trimDots(ary) {
+            var i, part;
+            for (i = 0; i < ary.length; i++) {
+                part = ary[i];
+                if (part === '.') {
+                    ary.splice(i, 1);
+                    i -= 1;
+                } else if (part === '..') {
+                    // If at the start, or previous value is still ..,
+                    // keep them so that when converted to a path it may
+                    // still work when converted to a path, even though
+                    // as an ID it is less than ideal. In larger point
+                    // releases, may be better to just kick out an error.
+                    if (i === 0 || (i == 1 && ary[2] === '..') || ary[i - 1] === '..') {
+                        continue;
+                    } else if (i > 0) {
+                        ary.splice(i - 1, 2);
+                        i -= 2;
+                    }
+                }
+            }
+        }
+
+        /**
+         * Given a relative module name, like ./something, normalize it to
+         * a real name that can be mapped to a path.
+         * @param {String} name the relative name
+         * @param {String} baseName a real name that the name arg is relative
+         * to.
+         * @param {Boolean} applyMap apply the map config to the value. Should
+         * only be done if this normalization is for a dependency ID.
+         * @returns {String} normalized name
+         */
+        function normalize(name, baseName, applyMap) {
+            var pkgMain, mapValue, nameParts, i, j, nameSegment, lastIndex,
+                foundMap, foundI, foundStarMap, starI, normalizedBaseParts,
+                baseParts = (baseName && baseName.split('/')),
+                map = config.map,
+                starMap = map && map['*'];
+
+            //Adjust any relative paths.
+            if (name) {
+                name = name.split('/');
+                lastIndex = name.length - 1;
+
+                // If wanting node ID compatibility, strip .js from end
+                // of IDs. Have to do this here, and not in nameToUrl
+                // because node allows either .js or non .js to map
+                // to same file.
+                if (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) {
+                    name[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, '');
+                }
+
+                // Starts with a '.' so need the baseName
+                if (name[0].charAt(0) === '.' && baseParts) {
+                    //Convert baseName to array, and lop off the last part,
+                    //so that . matches that 'directory' and not name of the baseName's
+                    //module. For instance, baseName of 'one/two/three', maps to
+                    //'one/two/three.js', but we want the directory, 'one/two' for
+                    //this normalization.
+                    normalizedBaseParts = baseParts.slice(0, baseParts.length - 1);
+                    name = normalizedBaseParts.concat(name);
+                }
+
+                trimDots(name);
+                name = name.join('/');
+            }
+
+            //Apply map config if available.
+            if (applyMap && map && (baseParts || starMap)) {
+                nameParts = name.split('/');
+
+                outerLoop: for (i = nameParts.length; i > 0; i -= 1) {
+                    nameSegment = nameParts.slice(0, i).join('/');
+
+                    if (baseParts) {
+                        //Find the longest baseName segment match in the config.
+                        //So, do joins on the biggest to smallest lengths of baseParts.
+                        for (j = baseParts.length; j > 0; j -= 1) {
+                            mapValue = getOwn(map, baseParts.slice(0, j).join('/'));
+
+                            //baseName segment has config, find if it has one for
+                            //this name.
+                            if (mapValue) {
+                                mapValue = getOwn(mapValue, nameSegment);
+                                if (mapValue) {
+                                    //Match, update name to the new value.
+                                    foundMap = mapValue;
+                                    foundI = i;
+                                    break outerLoop;
+                                }
+                            }
+                        }
+                    }
+
+                    //Check for a star map match, but just hold on to it,
+                    //if there is a shorter segment match later in a matching
+                    //config, then favor over this star map.
+                    if (!foundStarMap && starMap && getOwn(starMap, nameSegment)) {
+                        foundStarMap = getOwn(starMap, nameSegment);
+                        starI = i;
+                    }
+                }
+
+                if (!foundMap && foundStarMap) {
+                    foundMap = foundStarMap;
+                    foundI = starI;
+                }
+
+                if (foundMap) {
+                    nameParts.splice(0, foundI, foundMap);
+                    name = nameParts.join('/');
+                }
+            }
+
+            // If the name points to a package's name, use
+            // the package main instead.
+            pkgMain = getOwn(config.pkgs, name);
+
+            return pkgMain ? pkgMain : name;
+        }
+
+        function removeScript(name) {
+            if (isBrowser) {
+                each(scripts(), function (scriptNode) {
+                    if (scriptNode.getAttribute('data-requiremodule') === name &&
+                            scriptNode.getAttribute('data-requirecontext') === context.contextName) {
+                        scriptNode.parentNode.removeChild(scriptNode);
+                        return true;
+                    }
+                });
+            }
+        }
+
+        function hasPathFallback(id) {
+            var pathConfig = getOwn(config.paths, id);
+            if (pathConfig && isArray(pathConfig) && pathConfig.length > 1) {
+                //Pop off the first array value, since it failed, and
+                //retry
+                pathConfig.shift();
+                context.require.undef(id);
+
+                //Custom require that does not do map translation, since
+                //ID is "absolute", already mapped/resolved.
+                context.makeRequire(null, {
+                    skipMap: true
+                })([id]);
+
+                return true;
+            }
+        }
+
+        //Turns a plugin!resource to [plugin, resource]
+        //with the plugin being undefined if the name
+        //did not have a plugin prefix.
+        function splitPrefix(name) {
+            var prefix,
+                index = name ? name.indexOf('!') : -1;
+            if (index > -1) {
+                prefix = name.substring(0, index);
+                name = name.substring(index + 1, name.length);
+            }
+            return [prefix, name];
+        }
+
+        /**
+         * Creates a module mapping that includes plugin prefix, module
+         * name, and path. If parentModuleMap is provided it will
+         * also normalize the name via require.normalize()
+         *
+         * @param {String} name the module name
+         * @param {String} [parentModuleMap] parent module map
+         * for the module name, used to resolve relative names.
+         * @param {Boolean} isNormalized: is the ID already normalized.
+         * This is true if this call is done for a define() module ID.
+         * @param {Boolean} applyMap: apply the map config to the ID.
+         * Should only be true if this map is for a dependency.
+         *
+         * @returns {Object}
+         */
+        function makeModuleMap(name, parentModuleMap, isNormalized, applyMap) {
+            var url, pluginModule, suffix, nameParts,
+                prefix = null,
+                parentName = parentModuleMap ? parentModuleMap.name : null,
+                originalName = name,
+                isDefine = true,
+                normalizedName = '';
+
+            //If no name, then it means it is a require call, generate an
+            //internal name.
+            if (!name) {
+                isDefine = false;
+                name = '_@r' + (requireCounter += 1);
+            }
+
+            nameParts = splitPrefix(name);
+            prefix = nameParts[0];
+            name = nameParts[1];
+
+            if (prefix) {
+                prefix = normalize(prefix, parentName, applyMap);
+                pluginModule = getOwn(defined, prefix);
+            }
+
+            //Account for relative paths if there is a base name.
+            if (name) {
+                if (prefix) {
+                    if (pluginModule && pluginModule.normalize) {
+                        //Plugin is loaded, use its normalize method.
+                        normalizedName = pluginModule.normalize(name, function (name) {
+                            return normalize(name, parentName, applyMap);
+                        });
+                    } else {
+                        // If nested plugin references, then do not try to
+                        // normalize, as it will not normalize correctly. This
+                        // places a restriction on resourceIds, and the longer
+                        // term solution is not to normalize until plugins are
+                        // loaded and all normalizations to allow for async
+                        // loading of a loader plugin. But for now, fixes the
+                        // common uses. Details in #1131
+                        normalizedName = name.indexOf('!') === -1 ?
+                                         normalize(name, parentName, applyMap) :
+                                         name;
+                    }
+                } else {
+                    //A regular module.
+                    normalizedName = normalize(name, parentName, applyMap);
+
+                    //Normalized name may be a plugin ID due to map config
+                    //application in normalize. The map config values must
+                    //already be normalized, so do not need to redo that part.
+                    nameParts = splitPrefix(normalizedName);
+                    prefix = nameParts[0];
+                    normalizedName = nameParts[1];
+                    isNormalized = true;
+
+                    url = context.nameToUrl(normalizedName);
+                }
+            }
+
+            //If the id is a plugin id that cannot be determined if it needs
+            //normalization, stamp it with a unique ID so two matching relative
+            //ids that may conflict can be separate.
+            suffix = prefix && !pluginModule && !isNormalized ?
+                     '_unnormalized' + (unnormalizedCounter += 1) :
+                     '';
+
+            return {
+                prefix: prefix,
+                name: normalizedName,
+                parentMap: parentModuleMap,
+                unnormalized: !!suffix,
+                url: url,
+                originalName: originalName,
+                isDefine: isDefine,
+                id: (prefix ?
+                        prefix + '!' + normalizedName :
+                        normalizedName) + suffix
+            };
+        }
+
+        function getModule(depMap) {
+            var id = depMap.id,
+                mod = getOwn(registry, id);
+
+            if (!mod) {
+                mod = registry[id] = new context.Module(depMap);
+            }
+
+            return mod;
+        }
+
+        function on(depMap, name, fn) {
+            var id = depMap.id,
+                mod = getOwn(registry, id);
+
+            if (hasProp(defined, id) &&
+                    (!mod || mod.defineEmitComplete)) {
+                if (name === 'defined') {
+                    fn(defined[id]);
+                }
+            } else {
+                mod = getModule(depMap);
+                if (mod.error && name === 'error') {
+                    fn(mod.error);
+                } else {
+                    mod.on(name, fn);
+                }
+            }
+        }
+
+        function onError(err, errback) {
+            var ids = err.requireModules,
+                notified = false;
+
+            if (errback) {
+                errback(err);
+            } else {
+                each(ids, function (id) {
+                    var mod = getOwn(registry, id);
+                    if (mod) {
+                        //Set error on module, so it skips timeout checks.
+                        mod.error = err;
+                        if (mod.events.error) {
+                            notified = true;
+                            mod.emit('error', err);
+                        }
+                    }
+                });
+
+                if (!notified) {
+                    req.onError(err);
+                }
+            }
+        }
+
+        /**
+         * Internal method to transfer globalQueue items to this context's
+         * defQueue.
+         */
+        function takeGlobalQueue() {
+            //Push all the globalDefQueue items into the context's defQueue
+            if (globalDefQueue.length) {
+                //Array splice in the values since the context code has a
+                //local var ref to defQueue, so cannot just reassign the one
+                //on context.
+                apsp.apply(defQueue,
+                           [defQueue.length, 0].concat(globalDefQueue));
+                globalDefQueue = [];
+            }
+        }
+
+        handlers = {
+            'require': function (mod) {
+                if (mod.require) {
+                    return mod.require;
+                } else {
+                    return (mod.require = context.makeRequire(mod.map));
+                }
+            },
+            'exports': function (mod) {
+                mod.usingExports = true;
+                if (mod.map.isDefine) {
+                    if (mod.exports) {
+                        return (defined[mod.map.id] = mod.exports);
+                    } else {
+                        return (mod.exports = defined[mod.map.id] = {});
+                    }
+                }
+            },
+            'module': function (mod) {
+                if (mod.module) {
+                    return mod.module;
+                } else {
+                    return (mod.module = {
+                        id: mod.map.id,
+                        uri: mod.map.url,
+                        config: function () {
+                            return  getOwn(config.config, mod.map.id) || {};
+                        },
+                        exports: mod.exports || (mod.exports = {})
+                    });
+                }
+            }
+        };
+
+        function cleanRegistry(id) {
+            //Clean up machinery used for waiting modules.
+            delete registry[id];
+            delete enabledRegistry[id];
+        }
+
+        function breakCycle(mod, traced, processed) {
+            var id = mod.map.id;
+
+            if (mod.error) {
+                mod.emit('error', mod.error);
+            } else {
+                traced[id] = true;
+                each(mod.depMaps, function (depMap, i) {
+                    var depId = depMap.id,
+                        dep = getOwn(registry, depId);
+
+                    //Only force things that have not completed
+                    //being defined, so still in the registry,
+                    //and only if it has not been matched up
+                    //in the module already.
+                    if (dep && !mod.depMatched[i] && !processed[depId]) {
+                        if (getOwn(traced, depId)) {
+                            mod.defineDep(i, defined[depId]);
+                            mod.check(); //pass false?
+                        } else {
+                            breakCycle(dep, traced, processed);
+                        }
+                    }
+                });
+                processed[id] = true;
+            }
+        }
+
+        function checkLoaded() {
+            var err, usingPathFallback,
+                waitInterval = config.waitSeconds * 1000,
+                //It is possible to disable the wait interval by using waitSeconds of 0.
+                expired = waitInterval && (context.startTime + waitInterval) < new Date().getTime(),
+                noLoads = [],
+                reqCalls = [],
+                stillLoading = false,
+                needCycleCheck = true;
+
+            //Do not bother if this call was a result of a cycle break.
+            if (inCheckLoaded) {
+                return;
+            }
+
+            inCheckLoaded = true;
+
+            //Figure out the state of all the modules.
+            eachProp(enabledRegistry, function (mod) {
+                var map = mod.map,
+                    modId = map.id;
+
+                //Skip things that are not enabled or in error state.
+                if (!mod.enabled) {
+                    return;
+                }
+
+                if (!map.isDefine) {
+                    reqCalls.push(mod);
+                }
+
+                if (!mod.error) {
+                    //If the module should be executed, and it has not
+                    //been inited and time is up, remember it.
+                    if (!mod.inited && expired) {
+                        if (hasPathFallback(modId)) {
+                            usingPathFallback = true;
+                            stillLoading = true;
+                        } else {
+                            noLoads.push(modId);
+                            removeScript(modId);
+                        }
+                    } else if (!mod.inited && mod.fetched && map.isDefine) {
+                        stillLoading = true;
+                        if (!map.prefix) {
+                            //No reason to keep looking for unfinished
+                            //loading. If the only stillLoading is a
+                            //plugin resource though, keep going,
+                            //because it may be that a plugin resource
+                            //is waiting on a non-plugin cycle.
+                            return (needCycleCheck = false);
+                        }
+                    }
+                }
+            });
+
+            if (expired && noLoads.length) {
+                //If wait time expired, throw error of unloaded modules.
+                err = makeError('timeout', 'Load timeout for modules: ' + noLoads, null, noLoads);
+                err.contextName = context.contextName;
+                return onError(err);
+            }
+
+            //Not expired, check for a cycle.
+            if (needCycleCheck) {
+                each(reqCalls, function (mod) {
+                    breakCycle(mod, {}, {});
+                });
+            }
+
+            //If still waiting on loads, and the waiting load is something
+            //other than a plugin resource, or there are still outstanding
+            //scripts, then just try back later.
+            if ((!expired || usingPathFallback) && stillLoading) {
+                //Something is still waiting to load. Wait for it, but only
+                //if a timeout is not already in effect.
+                if ((isBrowser || isWebWorker) && !checkLoadedTimeoutId) {
+                    checkLoadedTimeoutId = setTimeout(function () {
+                        checkLoadedTimeoutId = 0;
+                        checkLoaded();
+                    }, 50);
+                }
+            }
+
+            inCheckLoaded = false;
+        }
+
+        Module = function (map) {
+            this.events = getOwn(undefEvents, map.id) || {};
+            this.map = map;
+            this.shim = getOwn(config.shim, map.id);
+            this.depExports = [];
+            this.depMaps = [];
+            this.depMatched = [];
+            this.pluginMaps = {};
+            this.depCount = 0;
+
+            /* this.exports this.factory
+               this.depMaps = [],
+               this.enabled, this.fetched
+            */
+        };
+
+        Module.prototype = {
+            init: function (depMaps, factory, errback, options) {
+                options = options || {};
+
+                //Do not do more inits if already done. Can happen if there
+                //are multiple define calls for the same module. That is not
+                //a normal, common case, but it is also not unexpected.
+                if (this.inited) {
+                    return;
+                }
+
+                this.factory = factory;
+
+                if (errback) {
+                    //Register for errors on this module.
+                    this.on('error', errback);
+                } else if (this.events.error) {
+                    //If no errback already, but there are error listeners
+                    //on this module, set up an errback to pass to the deps.
+                    errback = bind(this, function (err) {
+                        this.emit('error', err);
+                    });
+                }
+
+                //Do a copy of the dependency array, so that
+                //source inputs are not modified. For example
+                //"shim" deps are passed in here directly, and
+                //doing a direct modification of the depMaps array
+                //would affect that config.
+                this.depMaps = depMaps && depMaps.slice(0);
+
+                this.errback = errback;
+
+                //Indicate this module has be initialized
+                this.inited = true;
+
+                this.ignore = options.ignore;
+
+                //Could have option to init this module in enabled mode,
+                //or could have been previously marked as enabled. However,
+                //the dependencies are not known until init is called. So
+                //if enabled previously, now trigger dependencies as enabled.
+                if (options.enabled || this.enabled) {
+                    //Enable this module and dependencies.
+                    //Will call this.check()
+                    this.enable();
+                } else {
+                    this.check();
+                }
+            },
+
+            defineDep: function (i, depExports) {
+                //Because of cycles, defined callback for a given
+                //export can be called more than once.
+                if (!this.depMatched[i]) {
+                    this.depMatched[i] = true;
+                    this.depCount -= 1;
+                    this.depExports[i] = depExports;
+                }
+            },
+
+            fetch: function () {
+                if (this.fetched) {
+                    return;
+                }
+                this.fetched = true;
+
+                context.startTime = (new Date()).getTime();
+
+                var map = this.map;
+
+                //If the manager is for a plugin managed resource,
+                //ask the plugin to load it now.
+                if (this.shim) {
+                    context.makeRequire(this.map, {
+                        enableBuildCallback: true
+                    })(this.shim.deps || [], bind(this, function () {
+                        return map.prefix ? this.callPlugin() : this.load();
+                    }));
+                } else {
+                    //Regular dependency.
+                    return map.prefix ? this.callPlugin() : this.load();
+                }
+            },
+
+            load: function () {
+                var url = this.map.url;
+
+                //Regular dependency.
+                if (!urlFetched[url]) {
+                    urlFetched[url] = true;
+                    context.load(this.map.id, url);
+                }
+            },
+
+            /**
+             * Checks if the module is ready to define itself, and if so,
+             * define it.
+             */
+            check: function () {
+                if (!this.enabled || this.enabling) {
+                    return;
+                }
+
+                var err, cjsModule,
+                    id = this.map.id,
+                    depExports = this.depExports,
+                    exports = this.exports,
+                    factory = this.factory;
+
+                if (!this.inited) {
+                    this.fetch();
+                } else if (this.error) {
+                    this.emit('error', this.error);
+                } else if (!this.defining) {
+                    //The factory could trigger another require call
+                    //that would result in checking this module to
+                    //define itself again. If already in the process
+                    //of doing that, skip this work.
+                    this.defining = true;
+
+                    if (this.depCount < 1 && !this.defined) {
+                        if (isFunction(factory)) {
+                            //If there is an error listener, favor passing
+                            //to that instead of throwing an error. However,
+                            //only do it for define()'d  modules. require
+                            //errbacks should not be called for failures in
+                            //their callbacks (#699). However if a global
+                            //onError is set, use that.
+                            if ((this.events.error && this.map.isDefine) ||
+                                req.onError !== defaultOnError) {
+                                try {
+                                    exports = context.execCb(id, factory, depExports, exports);
+                                } catch (e) {
+                                    err = e;
+                                }
+                            } else {
+                                exports = context.execCb(id, factory, depExports, exports);
+                            }
+
+                            // Favor return value over exports. If node/cjs in play,
+                            // then will not have a return value anyway. Favor
+                            // module.exports assignment over exports object.
+                            if (this.map.isDefine && exports === undefined) {
+                                cjsModule = this.module;
+                                if (cjsModule) {
+                                    exports = cjsModule.exports;
+                                } else if (this.usingExports) {
+                                    //exports already set the defined value.
+                                    exports = this.exports;
+                                }
+                            }
+
+                            if (err) {
+                                err.requireMap = this.map;
+                                err.requireModules = this.map.isDefine ? [this.map.id] : null;
+                                err.requireType = this.map.isDefine ? 'define' : 'require';
+                                return onError((this.error = err));
+                            }
+
+                        } else {
+                            //Just a literal value
+                            exports = factory;
+                        }
+
+                        this.exports = exports;
+
+                        if (this.map.isDefine && !this.ignore) {
+                            defined[id] = exports;
+
+                            if (req.onResourceLoad) {
+                                req.onResourceLoad(context, this.map, this.depMaps);
+                            }
+                        }
+
+                        //Clean up
+                        cleanRegistry(id);
+
+                        this.defined = true;
+                    }
+
+                    //Finished the define stage. Allow calling check again
+                    //to allow define notifications below in the case of a
+                    //cycle.
+                    this.defining = false;
+
+                    if (this.defined && !this.defineEmitted) {
+                        this.defineEmitted = true;
+                        this.emit('defined', this.exports);
+                        this.defineEmitComplete = true;
+                    }
+
+                }
+            },
+
+            callPlugin: function () {
+                var map = this.map,
+                    id = map.id,
+                    //Map already normalized the prefix.
+                    pluginMap = makeModuleMap(map.prefix);
+
+                //Mark this as a dependency for this plugin, so it
+                //can be traced for cycles.
+                this.depMaps.push(pluginMap);
+
+                on(pluginMap, 'defined', bind(this, function (plugin) {
+                    var load, normalizedMap, normalizedMod,
+                        bundleId = getOwn(bundlesMap, this.map.id),
+                        name = this.map.name,
+                        parentName = this.map.parentMap ? this.map.parentMap.name : null,
+                        localRequire = context.makeRequire(map.parentMap, {
+                            enableBuildCallback: true
+                        });
+
+                    //If current map is not normalized, wait for that
+                    //normalized name to load instead of continuing.
+                    if (this.map.unnormalized) {
+                        //Normalize the ID if the plugin allows it.
+                        if (plugin.normalize) {
+                            name = plugin.normalize(name, function (name) {
+                                return normalize(name, parentName, true);
+                            }) || '';
+                        }
+
+                        //prefix and name should already be normalized, no need
+                        //for applying map config again either.
+                        normalizedMap = makeModuleMap(map.prefix + '!' + name,
+                                                      this.map.parentMap);
+                        on(normalizedMap,
+                            'defined', bind(this, function (value) {
+                                this.init([], function () { return value; }, null, {
+                                    enabled: true,
+                                    ignore: true
+                                });
+                            }));
+
+                        normalizedMod = getOwn(registry, normalizedMap.id);
+                        if (normalizedMod) {
+                            //Mark this as a dependency for this plugin, so it
+                            //can be traced for cycles.
+                            this.depMaps.push(normalizedMap);
+
+                            if (this.events.error) {
+                                normalizedMod.on('error', bind(this, function (err) {
+                                    this.emit('error', err);
+                                }));
+                            }
+                            normalizedMod.enable();
+                        }
+
+                        return;
+                    }
+
+                    //If a paths config, then just load that file instead to
+                    //resolve the plugin, as it is built into that paths layer.
+                    if (bundleId) {
+                        this.map.url = context.nameToUrl(bundleId);
+                        this.load();
+                        return;
+                    }
+
+                    load = bind(this, function (value) {
+                        this.init([], function () { return value; }, null, {
+                            enabled: true
+                        });
+                    });
+
+                    load.error = bind(this, function (err) {
+                        this.inited = true;
+                        this.error = err;
+                        err.requireModules = [id];
+
+                        //Remove temp unnormalized modules for this module,
+                        //since they will never be resolved otherwise now.
+                        eachProp(registry, function (mod) {
+                            if (mod.map.id.indexOf(id + '_unnormalized') === 0) {
+                                cleanRegistry(mod.map.id);
+                            }
+                        });
+
+                        onError(err);
+                    });
+
+                    //Allow plugins to load other code without having to know the
+                    //context or how to 'complete' the load.
+                    load.fromText = bind(this, function (text, textAlt) {
+                        /*jslint evil: true */
+                        var moduleName = map.name,
+                            moduleMap = makeModuleMap(moduleName),
+                            hasInteractive = useInteractive;
+
+                        //As of 2.1.0, support just passing the text, to reinforce
+                        //fromText only being called once per resource. Still
+                        //support old style of passing moduleName but discard
+                        //that moduleName in favor of the internal ref.
+                        if (textAlt) {
+                            text = textAlt;
+                        }
+
+                        //Turn off interactive script matching for IE for any define
+                        //calls in the text, then turn it back on at the end.
+                        if (hasInteractive) {
+                            useInteractive = false;
+                        }
+
+                        //Prime the system by creating a module instance for
+                        //it.
+                        getModule(moduleMap);
+
+                        //Transfer any config to this other module.
+                        if (hasProp(config.config, id)) {
+                            config.config[moduleName] = config.config[id];
+                        }
+
+                        try {
+                            req.exec(text);
+                        } catch (e) {
+                            return onError(makeError('fromtexteval',
+                                             'fromText eval for ' + id +
+                                            ' failed: ' + e,
+                                             e,
+                                             [id]));
+                        }
+
+                        if (hasInteractive) {
+                            useInteractive = true;
+                        }
+
+                        //Mark this as a dependency for the plugin
+                        //resource
+                        this.depMaps.push(moduleMap);
+
+                        //Support anonymous modules.
+                        context.completeLoad(moduleName);
+
+                        //Bind the value of that module to the value for this
+                        //resource ID.
+                        localRequire([moduleName], load);
+                    });
+
+                    //Use parentName here since the plugin's name is not reliable,
+                    //could be some weird string with no path that actually wants to
+                    //reference the parentName's path.
+                    plugin.load(map.name, localRequire, load, config);
+                }));
+
+                context.enable(pluginMap, this);
+                this.pluginMaps[pluginMap.id] = pluginMap;
+            },
+
+            enable: function () {
+                enabledRegistry[this.map.id] = this;
+                this.enabled = true;
+
+                //Set flag mentioning that the module is enabling,
+                //so that immediate calls to the defined callbacks
+                //for dependencies do not trigger inadvertent load
+                //with the depCount still being zero.
+                this.enabling = true;
+
+                //Enable each dependency
+                each(this.depMaps, bind(this, function (depMap, i) {
+                    var id, mod, handler;
+
+                    if (typeof depMap === 'string') {
+                        //Dependency needs to be converted to a depMap
+                        //and wired up to this module.
+                        depMap = makeModuleMap(depMap,
+                                               (this.map.isDefine ? this.map : this.map.parentMap),
+                                               false,
+                                               !this.skipMap);
+                        this.depMaps[i] = depMap;
+
+                        handler = getOwn(handlers, depMap.id);
+
+                        if (handler) {
+                            this.depExports[i] = handler(this);
+                            return;
+                        }
+
+                        this.depCount += 1;
+
+                        on(depMap, 'defined', bind(this, function (depExports) {
+                            this.defineDep(i, depExports);
+                            this.check();
+                        }));
+
+                        if (this.errback) {
+                            on(depMap, 'error', bind(this, this.errback));
+                        }
+                    }
+
+                    id = depMap.id;
+                    mod = registry[id];
+
+                    //Skip special modules like 'require', 'exports', 'module'
+                    //Also, don't call enable if it is already enabled,
+                    //important in circular dependency cases.
+                    if (!hasProp(handlers, id) && mod && !mod.enabled) {
+                        context.enable(depMap, this);
+                    }
+                }));
+
+                //Enable each plugin that is used in
+                //a dependency
+                eachProp(this.pluginMaps, bind(this, function (pluginMap) {
+                    var mod = getOwn(registry, pluginMap.id);
+                    if (mod && !mod.enabled) {
+                        context.enable(pluginMap, this);
+                    }
+                }));
+
+                this.enabling = false;
+
+                this.check();
+            },
+
+            on: function (name, cb) {
+                var cbs = this.events[name];
+                if (!cbs) {
+                    cbs = this.events[name] = [];
+                }
+                cbs.push(cb);
+            },
+
+            emit: function (name, evt) {
+                each(this.events[name], function (cb) {
+                    cb(evt);
+                });
+                if (name === 'error') {
+                    //Now that the error handler was triggered, remove
+                    //the listeners, since this broken Module instance
+                    //can stay around for a while in the registry.
+                    delete this.events[name];
+                }
+            }
+        };
+
+        function callGetModule(args) {
+            //Skip modules already defined.
+            if (!hasProp(defined, args[0])) {
+                getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]);
+            }
+        }
+
+        function removeListener(node, func, name, ieName) {
+            //Favor detachEvent because of IE9
+            //issue, see attachEvent/addEventListener comment elsewhere
+            //in this file.
+            if (node.detachEvent && !isOpera) {
+                //Probably IE. If not it will throw an error, which will be
+                //useful to know.
+                if (ieName) {
+                    node.detachEvent(ieName, func);
+                }
+            } else {
+                node.removeEventListener(name, func, false);
+            }
+        }
+
+        /**
+         * Given an event from a script node, get the requirejs info from it,
+         * and then removes the event listeners on the node.
+         * @param {Event} evt
+         * @returns {Object}
+         */
+        function getScriptData(evt) {
+            //Using currentTarget instead of target for Firefox 2.0's sake. Not
+            //all old browsers will be supported, but this one was easy enough
+            //to support and still makes sense.
+            var node = evt.currentTarget || evt.srcElement;
+
+            //Remove the listeners once here.
+            removeListener(node, context.onScriptLoad, 'load', 'onreadystatechange');
+            removeListener(node, context.onScriptError, 'error');
+
+            return {
+                node: node,
+                id: node && node.getAttribute('data-requiremodule')
+            };
+        }
+
+        function intakeDefines() {
+            var args;
+
+            //Any defined modules in the global queue, intake them now.
+            takeGlobalQueue();
+
+            //Make sure any remaining defQueue items get properly processed.
+            while (defQueue.length) {
+                args = defQueue.shift();
+                if (args[0] === null) {
+                    return onError(makeError('mismatch', 'Mismatched anonymous define() module: ' + args[args.length - 1]));
+                } else {
+                    //args are id, deps, factory. Should be normalized by the
+                    //define() function.
+                    callGetModule(args);
+                }
+            }
+        }
+
+        context = {
+            config: config,
+            contextName: contextName,
+            registry: registry,
+            defined: defined,
+            urlFetched: urlFetched,
+            defQueue: defQueue,
+            Module: Module,
+            makeModuleMap: makeModuleMap,
+            nextTick: req.nextTick,
+            onError: onError,
+
+            /**
+             * Set a configuration for the context.
+             * @param {Object} cfg config object to integrate.
+             */
+            configure: function (cfg) {
+                //Make sure the baseUrl ends in a slash.
+                if (cfg.baseUrl) {
+                    if (cfg.baseUrl.charAt(cfg.baseUrl.length - 1) !== '/') {
+                        cfg.baseUrl += '/';
+                    }
+                }
+
+                //Save off the paths since they require special processing,
+                //they are additive.
+                var shim = config.shim,
+                    objs = {
+                        paths: true,
+                        bundles: true,
+                        config: true,
+                        map: true
+                    };
+
+                eachProp(cfg, function (value, prop) {
+                    if (objs[prop]) {
+                        if (!config[prop]) {
+                            config[prop] = {};
+                        }
+                        mixin(config[prop], value, true, true);
+                    } else {
+                        config[prop] = value;
+                    }
+                });
+
+                //Reverse map the bundles
+                if (cfg.bundles) {
+                    eachProp(cfg.bundles, function (value, prop) {
+                        each(value, function (v) {
+                            if (v !== prop) {
+                                bundlesMap[v] = prop;
+                            }
+                        });
+                    });
+                }
+
+                //Merge shim
+                if (cfg.shim) {
+                    eachProp(cfg.shim, function (value, id) {
+                        //Normalize the structure
+                        if (isArray(value)) {
+                            value = {
+                                deps: value
+                            };
+                        }
+                        if ((value.exports || value.init) && !value.exportsFn) {
+                            value.exportsFn = context.makeShimExports(value);
+                        }
+                        shim[id] = value;
+                    });
+                    config.shim = shim;
+                }
+
+                //Adjust packages if necessary.
+                if (cfg.packages) {
+                    each(cfg.packages, function (pkgObj) {
+                        var location, name;
+
+                        pkgObj = typeof pkgObj === 'string' ? { name: pkgObj } : pkgObj;
+
+                        name = pkgObj.name;
+                        location = pkgObj.location;
+                        if (location) {
+                            config.paths[name] = pkgObj.location;
+                        }
+
+                        //Save pointer to main module ID for pkg name.
+                        //Remove leading dot in main, so main paths are normalized,
+                        //and remove any trailing .js, since different package
+                        //envs have different conventions: some use a module name,
+                        //some use a file name.
+                        config.pkgs[name] = pkgObj.name + '/' + (pkgObj.main || 'main')
+                                     .replace(currDirRegExp, '')
+                                     .replace(jsSuffixRegExp, '');
+                    });
+                }
+
+                //If there are any "waiting to execute" modules in the registry,
+                //update the maps for them, since their info, like URLs to load,
+                //may have changed.
+                eachProp(registry, function (mod, id) {
+                    //If module already has init called, since it is too
+                    //late to modify them, and ignore unnormalized ones
+                    //since they are transient.
+                    if (!mod.inited && !mod.map.unnormalized) {
+                        mod.map = makeModuleMap(id);
+                    }
+                });
+
+                //If a deps array or a config callback is specified, then call
+                //require with those args. This is useful when require is defined as a
+                //config object before require.js is loaded.
+                if (cfg.deps || cfg.callback) {
+                    context.require(cfg.deps || [], cfg.callback);
+                }
+            },
+
+            makeShimExports: function (value) {
+                function fn() {
+                    var ret;
+                    if (value.init) {
+                        ret = value.init.apply(global, arguments);
+                    }
+                    return ret || (value.exports && getGlobal(value.exports));
+                }
+                return fn;
+            },
+
+            makeRequire: function (relMap, options) {
+                options = options || {};
+
+                function localRequire(deps, callback, errback) {
+                    var id, map, requireMod;
+
+                    if (options.enableBuildCallback && callback && isFunction(callback)) {
+                        callback.__requireJsBuild = true;
+                    }
+
+                    if (typeof deps === 'string') {
+                        if (isFunction(callback)) {
+                            //Invalid call
+                            return onError(makeError('requireargs', 'Invalid require call'), errback);
+                        }
+
+                        //If require|exports|module are requested, get the
+                        //value for them from the special handlers. Caveat:
+                        //this only works while module is being defined.
+                        if (relMap && hasProp(handlers, deps)) {
+                            return handlers[deps](registry[relMap.id]);
+                        }
+
+                        //Synchronous access to one module. If require.get is
+                        //available (as in the Node adapter), prefer that.
+                        if (req.get) {
+                            return req.get(context, deps, relMap, localRequire);
+                        }
+
+                        //Normalize module name, if it contains . or ..
+                        map = makeModuleMap(deps, relMap, false, true);
+                        id = map.id;
+
+                        if (!hasProp(defined, id)) {
+                            return onError(makeError('notloaded', 'Module name "' +
+                                        id +
+                                        '" has not been loaded yet for context: ' +
+                                        contextName +
+                                        (relMap ? '' : '. Use require([])')));
+                        }
+                        return defined[id];
+                    }
+
+                    //Grab defines waiting in the global queue.
+                    intakeDefines();
+
+                    //Mark all the dependencies as needing to be loaded.
+                    context.nextTick(function () {
+                        //Some defines could have been added since the
+                        //require call, collect them.
+                        intakeDefines();
+
+                        requireMod = getModule(makeModuleMap(null, relMap));
+
+                        //Store if map config should be applied to this require
+                        //call for dependencies.
+                        requireMod.skipMap = options.skipMap;
+
+                        requireMod.init(deps, callback, errback, {
+                            enabled: true
+                        });
+
+                        checkLoaded();
+                    });
+
+                    return localRequire;
+                }
+
+                mixin(localRequire, {
+                    isBrowser: isBrowser,
+
+                    /**
+                     * Converts a module name + .extension into an URL path.
+                     * *Requires* the use of a module name. It does not support using
+                     * plain URLs like nameToUrl.
+                     */
+                    toUrl: function (moduleNamePlusExt) {
+                        var ext,
+                            index = moduleNamePlusExt.lastIndexOf('.'),
+                            segment = moduleNamePlusExt.split('/')[0],
+                            isRelative = segment === '.' || segment === '..';
+
+                        //Have a file extension alias, and it is not the
+                        //dots from a relative path.
+                        if (index !== -1 && (!isRelative || index > 1)) {
+                            ext = moduleNamePlusExt.substring(index, moduleNamePlusExt.length);
+                            moduleNamePlusExt = moduleNamePlusExt.substring(0, index);
+                        }
+
+                        return context.nameToUrl(normalize(moduleNamePlusExt,
+                                                relMap && relMap.id, true), ext,  true);
+                    },
+
+                    defined: function (id) {
+                        return hasProp(defined, makeModuleMap(id, relMap, false, true).id);
+                    },
+
+                    specified: function (id) {
+                        id = makeModuleMap(id, relMap, false, true).id;
+                        return hasProp(defined, id) || hasProp(registry, id);
+                    }
+                });
+
+                //Only allow undef on top level require calls
+                if (!relMap) {
+                    localRequire.undef = function (id) {
+                        //Bind any waiting define() calls to this context,
+                        //fix for #408
+                        takeGlobalQueue();
+
+                        var map = makeModuleMap(id, relMap, true),
+                            mod = getOwn(registry, id);
+
+                        removeScript(id);
+
+                        delete defined[id];
+                        delete urlFetched[map.url];
+                        delete undefEvents[id];
+
+                        //Clean queued defines too. Go backwards
+                        //in array so that the splices do not
+                        //mess up the iteration.
+                        eachReverse(defQueue, function(args, i) {
+                            if(args[0] === id) {
+                                defQueue.splice(i, 1);
+                            }
+                        });
+
+                        if (mod) {
+                            //Hold on to listeners in case the
+                            //module will be attempted to be reloaded
+                            //using a different config.
+                            if (mod.events.defined) {
+                                undefEvents[id] = mod.events;
+                            }
+
+                            cleanRegistry(id);
+                        }
+                    };
+                }
+
+                return localRequire;
+            },
+
+            /**
+             * Called to enable a module if it is still in the registry
+             * awaiting enablement. A second arg, parent, the parent module,
+             * is passed in for context, when this method is overridden by
+             * the optimizer. Not shown here to keep code compact.
+             */
+            enable: function (depMap) {
+                var mod = getOwn(registry, depMap.id);
+                if (mod) {
+                    getModule(depMap).enable();
+                }
+            },
+
+            /**
+             * Internal method used by environment adapters to complete a load event.
+             * A load event could be a script load or just a load pass from a synchronous
+             * load call.
+             * @param {String} moduleName the name of the module to potentially complete.
+             */
+            completeLoad: function (moduleName) {
+                var found, args, mod,
+                    shim = getOwn(config.shim, moduleName) || {},
+                    shExports = shim.exports;
+
+                takeGlobalQueue();
+
+                while (defQueue.length) {
+                    args = defQueue.shift();
+                    if (args[0] === null) {
+                        args[0] = moduleName;
+                        //If already found an anonymous module and bound it
+                        //to this name, then this is some other anon module
+                        //waiting for its completeLoad to fire.
+                        if (found) {
+                            break;
+                        }
+                        found = true;
+                    } else if (args[0] === moduleName) {
+                        //Found matching define call for this script!
+                        found = true;
+                    }
+
+                    callGetModule(args);
+                }
+
+                //Do this after the cycle of callGetModule in case the result
+                //of those calls/init calls changes the registry.
+                mod = getOwn(registry, moduleName);
+
+                if (!found && !hasProp(defined, moduleName) && mod && !mod.inited) {
+                    if (config.enforceDefine && (!shExports || !getGlobal(shExports))) {
+                        if (hasPathFallback(moduleName)) {
+                            return;
+                        } else {
+                            return onError(makeError('nodefine',
+                                             'No define call for ' + moduleName,
+                                             null,
+                                             [moduleName]));
+                        }
+                    } else {
+                        //A script that does not call define(), so just simulate
+                        //the call for it.
+                        callGetModule([moduleName, (shim.deps || []), shim.exportsFn]);
+                    }
+                }
+
+                checkLoaded();
+            },
+
+            /**
+             * Converts a module name to a file path. Supports cases where
+             * moduleName may actually be just an URL.
+             * Note that it **does not** call normalize on the moduleName,
+             * it is assumed to have already been normalized. This is an
+             * internal API, not a public one. Use toUrl for the public API.
+             */
+            nameToUrl: function (moduleName, ext, skipExt) {
+                var paths, syms, i, parentModule, url,
+                    parentPath, bundleId,
+                    pkgMain = getOwn(config.pkgs, moduleName);
+
+                if (pkgMain) {
+                    moduleName = pkgMain;
+                }
+
+                bundleId = getOwn(bundlesMap, moduleName);
+
+                if (bundleId) {
+                    return context.nameToUrl(bundleId, ext, skipExt);
+                }
+
+                //If a colon is in the URL, it indicates a protocol is used and it is just
+                //an URL to a file, or if it starts with a slash, contains a query arg (i.e. ?)
+                //or ends with .js, then assume the user meant to use an url and not a module id.
+                //The slash is important for protocol-less URLs as well as full paths.
+                if (req.jsExtRegExp.test(moduleName)) {
+                    //Just a plain path, not module name lookup, so just return it.
+                    //Add extension if it is included. This is a bit wonky, only non-.js things pass
+                    //an extension, this method probably needs to be reworked.
+                    url = moduleName + (ext || '');
+                } else {
+                    //A module that needs to be converted to a path.
+                    paths = config.paths;
+
+                    syms = moduleName.split('/');
+                    //For each module name segment, see if there is a path
+                    //registered for it. Start with most specific name
+                    //and work up from it.
+                    for (i = syms.length; i > 0; i -= 1) {
+                        parentModule = syms.slice(0, i).join('/');
+
+                        parentPath = getOwn(paths, parentModule);
+                        if (parentPath) {
+                            //If an array, it means there are a few choices,
+                            //Choose the one that is desired
+                            if (isArray(parentPath)) {
+                                parentPath = parentPath[0];
+                            }
+                            syms.splice(0, i, parentPath);
+                            break;
+                        }
+                    }
+
+                    //Join the path parts together, then figure out if baseUrl is needed.
+                    url = syms.join('/');
+                    url += (ext || (/^data\:|\?/.test(url) || skipExt ? '' : '.js'));
+                    url = (url.charAt(0) === '/' || url.match(/^[\w\+\.\-]+:/) ? '' : config.baseUrl) + url;
+                }
+
+                return config.urlArgs ? url +
+                                        ((url.indexOf('?') === -1 ? '?' : '&') +
+                                         config.urlArgs) : url;
+            },
+
+            //Delegates to req.load. Broken out as a separate function to
+            //allow overriding in the optimizer.
+            load: function (id, url) {
+                req.load(context, id, url);
+            },
+
+            /**
+             * Executes a module callback function. Broken out as a separate function
+             * solely to allow the build system to sequence the files in the built
+             * layer in the right sequence.
+             *
+             * @private
+             */
+            execCb: function (name, callback, args, exports) {
+                return callback.apply(exports, args);
+            },
+
+            /**
+             * callback for script loads, used to check status of loading.
+             *
+             * @param {Event} evt the event from the browser for the script
+             * that was loaded.
+             */
+            onScriptLoad: function (evt) {
+                //Using currentTarget instead of target for Firefox 2.0's sake. Not
+                //all old browsers will be supported, but this one was easy enough
+                //to support and still makes sense.
+                if (evt.type === 'load' ||
+                        (readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) {
+                    //Reset interactive script so a script node is not held onto for
+                    //to long.
+                    interactiveScript = null;
+
+                    //Pull out the name of the module and the context.
+                    var data = getScriptData(evt);
+                    context.completeLoad(data.id);
+                }
+            },
+
+            /**
+             * Callback for script errors.
+             */
+            onScriptError: function (evt) {
+                var data = getScriptData(evt);
+                if (!hasPathFallback(data.id)) {
+                    return onError(makeError('scripterror', 'Script error for: ' + data.id, evt, [data.id]));
+                }
+            }
+        };
+
+        context.require = context.makeRequire();
+        return context;
+    }
+
+    /**
+     * Main entry point.
+     *
+     * If the only argument to require is a string, then the module that
+     * is represented by that string is fetched for the appropriate context.
+     *
+     * If the first argument is an array, then it will be treated as an array
+     * of dependency string names to fetch. An optional function callback can
+     * be specified to execute when all of those dependencies are available.
+     *
+     * Make a local req variable to help Caja compliance (it assumes things
+     * on a require that are not standardized), and to give a short
+     * name for minification/local scope use.
+     */
+    req = requirejs = function (deps, callback, errback, optional) {
+
+        //Find the right context, use default
+        var context, config,
+            contextName = defContextName;
+
+        // Determine if have config object in the call.
+        if (!isArray(deps) && typeof deps !== 'string') {
+            // deps is a config object
+            config = deps;
+            if (isArray(callback)) {
+                // Adjust args if there are dependencies
+                deps = callback;
+                callback = errback;
+                errback = optional;
+            } else {
+                deps = [];
+            }
+        }
+
+        if (config && config.context) {
+            contextName = config.context;
+        }
+
+        context = getOwn(contexts, contextName);
+        if (!context) {
+            context = contexts[contextName] = req.s.newContext(contextName);
+        }
+
+        if (config) {
+            context.configure(config);
+        }
+
+        return context.require(deps, callback, errback);
+    };
+
+    /**
+     * Support require.config() to make it easier to cooperate with other
+     * AMD loaders on globally agreed names.
+     */
+    req.config = function (config) {
+        return req(config);
+    };
+
+    /**
+     * Execute something after the current tick
+     * of the event loop. Override for other envs
+     * that have a better solution than setTimeout.
+     * @param  {Function} fn function to execute later.
+     */
+    req.nextTick = typeof setTimeout !== 'undefined' ? function (fn) {
+        setTimeout(fn, 4);
+    } : function (fn) { fn(); };
+
+    /**
+     * Export require as a global, but only if it does not already exist.
+     */
+    if (!require) {
+        require = req;
+    }
+
+    req.version = version;
+
+    //Used to filter out dependencies that are already paths.
+    req.jsExtRegExp = /^\/|:|\?|\.js$/;
+    req.isBrowser = isBrowser;
+    s = req.s = {
+        contexts: contexts,
+        newContext: newContext
+    };
+
+    //Create default context.
+    req({});
+
+    //Exports some context-sensitive methods on global require.
+    each([
+        'toUrl',
+        'undef',
+        'defined',
+        'specified'
+    ], function (prop) {
+        //Reference from contexts instead of early binding to default context,
+        //so that during builds, the latest instance of the default context
+        //with its config gets used.
+        req[prop] = function () {
+            var ctx = contexts[defContextName];
+            return ctx.require[prop].apply(ctx, arguments);
+        };
+    });
+
+    if (isBrowser) {
+        head = s.head = document.getElementsByTagName('head')[0];
+        //If BASE tag is in play, using appendChild is a problem for IE6.
+        //When that browser dies, this can be removed. Details in this jQuery bug:
+        //http://dev.jquery.com/ticket/2709
+        baseElement = document.getElementsByTagName('base')[0];
+        if (baseElement) {
+            head = s.head = baseElement.parentNode;
+        }
+    }
+
+    /**
+     * Any errors that require explicitly generates will be passed to this
+     * function. Intercept/override it if you want custom error handling.
+     * @param {Error} err the error object.
+     */
+    req.onError = defaultOnError;
+
+    /**
+     * Creates the node for the load command. Only used in browser envs.
+     */
+    req.createNode = function (config, moduleName, url) {
+        var node = config.xhtml ?
+                document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') :
+                document.createElement('script');
+        node.type = config.scriptType || 'text/javascript';
+        node.charset = 'utf-8';
+        node.async = true;
+        return node;
+    };
+
+    /**
+     * Does the request to load a module for the browser case.
+     * Make this a separate function to allow other environments
+     * to override it.
+     *
+     * @param {Object} context the require context to find state.
+     * @param {String} moduleName the name of the module.
+     * @param {Object} url the URL to the module.
+     */
+    req.load = function (context, moduleName, url) {
+        var config = (context && context.config) || {},
+            node;
+        if (isBrowser) {
+            //In the browser so use a script tag
+            node = req.createNode(config, moduleName, url);
+
+            node.setAttribute('data-requirecontext', context.contextName);
+            node.setAttribute('data-requiremodule', moduleName);
+
+            //Set up load listener. Test attachEvent first because IE9 has
+            //a subtle issue in its addEventListener and script onload firings
+            //that do not match the behavior of all other browsers with
+            //addEventListener support, which fire the onload event for a
+            //script right after the script execution. See:
+            //https://connect.microsoft.com/IE/feedback/details/648057/script-onload-event-is-not-fired-immediately-after-script-execution
+            //UNFORTUNATELY Opera implements attachEvent but does not follow the script
+            //script execution mode.
+            if (node.attachEvent &&
+                    //Check if node.attachEvent is artificially added by custom script or
+                    //natively supported by browser
+                    //read https://github.com/jrburke/requirejs/issues/187
+                    //if we can NOT find [native code] then it must NOT natively supported.
+                    //in IE8, node.attachEvent does not have toString()
+                    //Note the test for "[native code" with no closing brace, see:
+                    //https://github.com/jrburke/requirejs/issues/273
+                    !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) &&
+                    !isOpera) {
+                //Probably IE. IE (at least 6-8) do not fire
+                //script onload right after executing the script, so
+                //we cannot tie the anonymous define call to a name.
+                //However, IE reports the script as being in 'interactive'
+                //readyState at the time of the define call.
+                useInteractive = true;
+
+                node.attachEvent('onreadystatechange', context.onScriptLoad);
+                //It would be great to add an error handler here to catch
+                //404s in IE9+. However, onreadystatechange will fire before
+                //the error handler, so that does not help. If addEventListener
+                //is used, then IE will fire error before load, but we cannot
+                //use that pathway given the connect.microsoft.com issue
+                //mentioned above about not doing the 'script execute,
+                //then fire the script load event listener before execute
+                //next script' that other browsers do.
+                //Best hope: IE10 fixes the issues,
+                //and then destroys all installs of IE 6-9.
+                //node.attachEvent('onerror', context.onScriptError);
+            } else {
+                node.addEventListener('load', context.onScriptLoad, false);
+                node.addEventListener('error', context.onScriptError, false);
+            }
+            node.src = url;
+
+            //For some cache cases in IE 6-8, the script executes before the end
+            //of the appendChild execution, so to tie an anonymous define
+            //call to the module name (which is stored on the node), hold on
+            //to a reference to this node, but clear after the DOM insertion.
+            currentlyAddingScript = node;
+            if (baseElement) {
+                head.insertBefore(node, baseElement);
+            } else {
+                head.appendChild(node);
+            }
+            currentlyAddingScript = null;
+
+            return node;
+        } else if (isWebWorker) {
+            try {
+                //In a web worker, use importScripts. This is not a very
+                //efficient use of importScripts, importScripts will block until
+                //its script is downloaded and evaluated. However, if web workers
+                //are in play, the expectation that a build has been done so that
+                //only one script needs to be loaded anyway. This may need to be
+                //reevaluated if other use cases become common.
+                importScripts(url);
+
+                //Account for anonymous modules
+                context.completeLoad(moduleName);
+            } catch (e) {
+                context.onError(makeError('importscripts',
+                                'importScripts failed for ' +
+                                    moduleName + ' at ' + url,
+                                e,
+                                [moduleName]));
+            }
+        }
+    };
+
+    function getInteractiveScript() {
+        if (interactiveScript && interactiveScript.readyState === 'interactive') {
+            return interactiveScript;
+        }
+
+        eachReverse(scripts(), function (script) {
+            if (script.readyState === 'interactive') {
+                return (interactiveScript = script);
+            }
+        });
+        return interactiveScript;
+    }
+
+    //Look for a data-main script attribute, which could also adjust the baseUrl.
+    if (isBrowser && !cfg.skipDataMain) {
+        //Figure out baseUrl. Get it from the script tag with require.js in it.
+        eachReverse(scripts(), function (script) {
+            //Set the 'head' where we can append children by
+            //using the script's parent.
+            if (!head) {
+                head = script.parentNode;
+            }
+
+            //Look for a data-main attribute to set main script for the page
+            //to load. If it is there, the path to data main becomes the
+            //baseUrl, if it is not already set.
+            dataMain = script.getAttribute('data-main');
+            if (dataMain) {
+                //Preserve dataMain in case it is a path (i.e. contains '?')
+                mainScript = dataMain;
+
+                //Set final baseUrl if there is not already an explicit one.
+                if (!cfg.baseUrl) {
+                    //Pull off the directory of data-main for use as the
+                    //baseUrl.
+                    src = mainScript.split('/');
+                    mainScript = src.pop();
+                    subPath = src.length ? src.join('/')  + '/' : './';
+
+                    cfg.baseUrl = subPath;
+                }
+
+                //Strip off any trailing .js since mainScript is now
+                //like a module name.
+                mainScript = mainScript.replace(jsSuffixRegExp, '');
+
+                 //If mainScript is still a path, fall back to dataMain
+                if (req.jsExtRegExp.test(mainScript)) {
+                    mainScript = dataMain;
+                }
+
+                //Put the data-main script in the files to load.
+                cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript];
+
+                return true;
+            }
+        });
+    }
+
+    /**
+     * The function that handles definitions of modules. Differs from
+     * require() in that a string for the module should be the first argument,
+     * and the function to execute after dependencies are loaded should
+     * return a value to define the module corresponding to the first argument's
+     * name.
+     */
+    define = function (name, deps, callback) {
+        var node, context;
+
+        //Allow for anonymous modules
+        if (typeof name !== 'string') {
+            //Adjust args appropriately
+            callback = deps;
+            deps = name;
+            name = null;
+        }
+
+        //This module may not have dependencies
+        if (!isArray(deps)) {
+            callback = deps;
+            deps = null;
+        }
+
+        //If no name, and callback is a function, then figure out if it a
+        //CommonJS thing with dependencies.
+        if (!deps && isFunction(callback)) {
+            deps = [];
+            //Remove comments from the callback string,
+            //look for require calls, and pull them into the dependencies,
+            //but only if there are function args.
+            if (callback.length) {
+                callback
+                    .toString()
+                    .replace(commentRegExp, '')
+                    .replace(cjsRequireRegExp, function (match, dep) {
+                        deps.push(dep);
+                    });
+
+                //May be a CommonJS thing even without require calls, but still
+                //could use exports, and module. Avoid doing exports and module
+                //work though if it just needs require.
+                //REQUIRES the function to expect the CommonJS variables in the
+                //order listed below.
+                deps = (callback.length === 1 ? ['require'] : ['require', 'exports', 'module']).concat(deps);
+            }
+        }
+
+        //If in IE 6-8 and hit an anonymous define() call, do the interactive
+        //work.
+        if (useInteractive) {
+            node = currentlyAddingScript || getInteractiveScript();
+            if (node) {
+                if (!name) {
+                    name = node.getAttribute('data-requiremodule');
+                }
+                context = contexts[node.getAttribute('data-requirecontext')];
+            }
+        }
+
+        //Always save off evaluating the def call until the script onload handler.
+        //This allows multiple modules to be in a file without prematurely
+        //tracing dependencies, and allows for anonymous module support,
+        //where the module name is not known until the script onload event
+        //occurs. If no context, use the global queue, and get it processed
+        //in the onscript load callback.
+        (context ? context.defQueue : globalDefQueue).push([name, deps, callback]);
+    };
+
+    define.amd = {
+        jQuery: true
+    };
+
+
+    /**
+     * Executes the text. Normally just uses eval, but can be modified
+     * to use a better, environment-specific call. Only used for transpiling
+     * loader plugins, not for plain JS modules.
+     * @param {String} text the text to execute/evaluate.
+     */
+    req.exec = function (text) {
+        /*jslint evil: true */
+        return eval(text);
+    };
+
+    //Set up with config info.
+    req(cfg);
+}(this));
diff --git a/lib/requirejs/require.min.js b/lib/requirejs/require.min.js
new file mode 100644 (file)
index 0000000..be103a7
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ RequireJS 2.1.15 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved.
+ Available via the MIT or new BSD license.
+ see: http://github.com/jrburke/requirejs for details
+*/
+var requirejs,require,define;
+(function(ba){function G(b){return"[object Function]"===K.call(b)}function H(b){return"[object Array]"===K.call(b)}function v(b,c){if(b){var d;for(d=0;d<b.length&&(!b[d]||!c(b[d],d,b));d+=1);}}function T(b,c){if(b){var d;for(d=b.length-1;-1<d&&(!b[d]||!c(b[d],d,b));d-=1);}}function t(b,c){return fa.call(b,c)}function m(b,c){return t(b,c)&&b[c]}function B(b,c){for(var d in b)if(t(b,d)&&c(b[d],d))break}function U(b,c,d,e){c&&B(c,function(c,g){if(d||!t(b,g))e&&"object"===typeof c&&c&&!H(c)&&!G(c)&&!(c instanceof
+RegExp)?(b[g]||(b[g]={}),U(b[g],c,d,e)):b[g]=c});return b}function u(b,c){return function(){return c.apply(b,arguments)}}function ca(b){throw b;}function da(b){if(!b)return b;var c=ba;v(b.split("."),function(b){c=c[b]});return c}function C(b,c,d,e){c=Error(c+"\nhttp://requirejs.org/docs/errors.html#"+b);c.requireType=b;c.requireModules=e;d&&(c.originalError=d);return c}function ga(b){function c(a,k,b){var f,l,c,d,e,g,i,p,k=k&&k.split("/"),h=j.map,n=h&&h["*"];if(a){a=a.split("/");l=a.length-1;j.nodeIdCompat&&
+Q.test(a[l])&&(a[l]=a[l].replace(Q,""));"."===a[0].charAt(0)&&k&&(l=k.slice(0,k.length-1),a=l.concat(a));l=a;for(c=0;c<l.length;c++)if(d=l[c],"."===d)l.splice(c,1),c-=1;else if(".."===d&&!(0===c||1==c&&".."===l[2]||".."===l[c-1])&&0<c)l.splice(c-1,2),c-=2;a=a.join("/")}if(b&&h&&(k||n)){l=a.split("/");c=l.length;a:for(;0<c;c-=1){e=l.slice(0,c).join("/");if(k)for(d=k.length;0<d;d-=1)if(b=m(h,k.slice(0,d).join("/")))if(b=m(b,e)){f=b;g=c;break a}!i&&(n&&m(n,e))&&(i=m(n,e),p=c)}!f&&i&&(f=i,g=p);f&&(l.splice(0,
+g,f),a=l.join("/"))}return(f=m(j.pkgs,a))?f:a}function d(a){z&&v(document.getElementsByTagName("script"),function(k){if(k.getAttribute("data-requiremodule")===a&&k.getAttribute("data-requirecontext")===i.contextName)return k.parentNode.removeChild(k),!0})}function e(a){var k=m(j.paths,a);if(k&&H(k)&&1<k.length)return k.shift(),i.require.undef(a),i.makeRequire(null,{skipMap:!0})([a]),!0}function n(a){var k,c=a?a.indexOf("!"):-1;-1<c&&(k=a.substring(0,c),a=a.substring(c+1,a.length));return[k,a]}function p(a,
+k,b,f){var l,d,e=null,g=k?k.name:null,j=a,p=!0,h="";a||(p=!1,a="_@r"+(K+=1));a=n(a);e=a[0];a=a[1];e&&(e=c(e,g,f),d=m(r,e));a&&(e?h=d&&d.normalize?d.normalize(a,function(a){return c(a,g,f)}):-1===a.indexOf("!")?c(a,g,f):a:(h=c(a,g,f),a=n(h),e=a[0],h=a[1],b=!0,l=i.nameToUrl(h)));b=e&&!d&&!b?"_unnormalized"+(O+=1):"";return{prefix:e,name:h,parentMap:k,unnormalized:!!b,url:l,originalName:j,isDefine:p,id:(e?e+"!"+h:h)+b}}function s(a){var k=a.id,b=m(h,k);b||(b=h[k]=new i.Module(a));return b}function q(a,
+k,b){var f=a.id,c=m(h,f);if(t(r,f)&&(!c||c.defineEmitComplete))"defined"===k&&b(r[f]);else if(c=s(a),c.error&&"error"===k)b(c.error);else c.on(k,b)}function w(a,b){var c=a.requireModules,f=!1;if(b)b(a);else if(v(c,function(b){if(b=m(h,b))b.error=a,b.events.error&&(f=!0,b.emit("error",a))}),!f)g.onError(a)}function x(){R.length&&(ha.apply(A,[A.length,0].concat(R)),R=[])}function y(a){delete h[a];delete V[a]}function F(a,b,c){var f=a.map.id;a.error?a.emit("error",a.error):(b[f]=!0,v(a.depMaps,function(f,
+d){var e=f.id,g=m(h,e);g&&(!a.depMatched[d]&&!c[e])&&(m(b,e)?(a.defineDep(d,r[e]),a.check()):F(g,b,c))}),c[f]=!0)}function D(){var a,b,c=(a=1E3*j.waitSeconds)&&i.startTime+a<(new Date).getTime(),f=[],l=[],g=!1,h=!0;if(!W){W=!0;B(V,function(a){var i=a.map,j=i.id;if(a.enabled&&(i.isDefine||l.push(a),!a.error))if(!a.inited&&c)e(j)?g=b=!0:(f.push(j),d(j));else if(!a.inited&&(a.fetched&&i.isDefine)&&(g=!0,!i.prefix))return h=!1});if(c&&f.length)return a=C("timeout","Load timeout for modules: "+f,null,
+f),a.contextName=i.contextName,w(a);h&&v(l,function(a){F(a,{},{})});if((!c||b)&&g)if((z||ea)&&!X)X=setTimeout(function(){X=0;D()},50);W=!1}}function E(a){t(r,a[0])||s(p(a[0],null,!0)).init(a[1],a[2])}function I(a){var a=a.currentTarget||a.srcElement,b=i.onScriptLoad;a.detachEvent&&!Y?a.detachEvent("onreadystatechange",b):a.removeEventListener("load",b,!1);b=i.onScriptError;(!a.detachEvent||Y)&&a.removeEventListener("error",b,!1);return{node:a,id:a&&a.getAttribute("data-requiremodule")}}function J(){var a;
+for(x();A.length;){a=A.shift();if(null===a[0])return w(C("mismatch","Mismatched anonymous define() module: "+a[a.length-1]));E(a)}}var W,Z,i,L,X,j={waitSeconds:7,baseUrl:"./",paths:{},bundles:{},pkgs:{},shim:{},config:{}},h={},V={},$={},A=[],r={},S={},aa={},K=1,O=1;L={require:function(a){return a.require?a.require:a.require=i.makeRequire(a.map)},exports:function(a){a.usingExports=!0;if(a.map.isDefine)return a.exports?r[a.map.id]=a.exports:a.exports=r[a.map.id]={}},module:function(a){return a.module?
+a.module:a.module={id:a.map.id,uri:a.map.url,config:function(){return m(j.config,a.map.id)||{}},exports:a.exports||(a.exports={})}}};Z=function(a){this.events=m($,a.id)||{};this.map=a;this.shim=m(j.shim,a.id);this.depExports=[];this.depMaps=[];this.depMatched=[];this.pluginMaps={};this.depCount=0};Z.prototype={init:function(a,b,c,f){f=f||{};if(!this.inited){this.factory=b;if(c)this.on("error",c);else this.events.error&&(c=u(this,function(a){this.emit("error",a)}));this.depMaps=a&&a.slice(0);this.errback=
+c;this.inited=!0;this.ignore=f.ignore;f.enabled||this.enabled?this.enable():this.check()}},defineDep:function(a,b){this.depMatched[a]||(this.depMatched[a]=!0,this.depCount-=1,this.depExports[a]=b)},fetch:function(){if(!this.fetched){this.fetched=!0;i.startTime=(new Date).getTime();var a=this.map;if(this.shim)i.makeRequire(this.map,{enableBuildCallback:!0})(this.shim.deps||[],u(this,function(){return a.prefix?this.callPlugin():this.load()}));else return a.prefix?this.callPlugin():this.load()}},load:function(){var a=
+this.map.url;S[a]||(S[a]=!0,i.load(this.map.id,a))},check:function(){if(this.enabled&&!this.enabling){var a,b,c=this.map.id;b=this.depExports;var f=this.exports,l=this.factory;if(this.inited)if(this.error)this.emit("error",this.error);else{if(!this.defining){this.defining=!0;if(1>this.depCount&&!this.defined){if(G(l)){if(this.events.error&&this.map.isDefine||g.onError!==ca)try{f=i.execCb(c,l,b,f)}catch(d){a=d}else f=i.execCb(c,l,b,f);this.map.isDefine&&void 0===f&&((b=this.module)?f=b.exports:this.usingExports&&
+(f=this.exports));if(a)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",w(this.error=a)}else f=l;this.exports=f;if(this.map.isDefine&&!this.ignore&&(r[c]=f,g.onResourceLoad))g.onResourceLoad(i,this.map,this.depMaps);y(c);this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}else this.fetch()}},callPlugin:function(){var a=
+this.map,b=a.id,d=p(a.prefix);this.depMaps.push(d);q(d,"defined",u(this,function(f){var l,d;d=m(aa,this.map.id);var e=this.map.name,P=this.map.parentMap?this.map.parentMap.name:null,n=i.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(f.normalize&&(e=f.normalize(e,function(a){return c(a,P,!0)})||""),f=p(a.prefix+"!"+e,this.map.parentMap),q(f,"defined",u(this,function(a){this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),d=m(h,f.id)){this.depMaps.push(f);
+if(this.events.error)d.on("error",u(this,function(a){this.emit("error",a)}));d.enable()}}else d?(this.map.url=i.nameToUrl(d),this.load()):(l=u(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),l.error=u(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];B(h,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&y(a.map.id)});w(a)}),l.fromText=u(this,function(f,c){var d=a.name,e=p(d),P=M;c&&(f=c);P&&(M=!1);s(e);t(j.config,b)&&(j.config[d]=j.config[b]);try{g.exec(f)}catch(h){return w(C("fromtexteval",
+"fromText eval for "+b+" failed: "+h,h,[b]))}P&&(M=!0);this.depMaps.push(e);i.completeLoad(d);n([d],l)}),f.load(a.name,n,l,j))}));i.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){V[this.map.id]=this;this.enabling=this.enabled=!0;v(this.depMaps,u(this,function(a,b){var c,f;if("string"===typeof a){a=p(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=m(L,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;q(a,"defined",u(this,function(a){this.defineDep(b,
+a);this.check()}));this.errback&&q(a,"error",u(this,this.errback))}c=a.id;f=h[c];!t(L,c)&&(f&&!f.enabled)&&i.enable(a,this)}));B(this.pluginMaps,u(this,function(a){var b=m(h,a.id);b&&!b.enabled&&i.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){v(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};i={config:j,contextName:b,registry:h,defined:r,urlFetched:S,defQueue:A,Module:Z,makeModuleMap:p,
+nextTick:g.nextTick,onError:w,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=j.shim,c={paths:!0,bundles:!0,config:!0,map:!0};B(a,function(a,b){c[b]?(j[b]||(j[b]={}),U(j[b],a,!0,!0)):j[b]=a});a.bundles&&B(a.bundles,function(a,b){v(a,function(a){a!==b&&(aa[a]=b)})});a.shim&&(B(a.shim,function(a,c){H(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=i.makeShimExports(a);b[c]=a}),j.shim=b);a.packages&&v(a.packages,function(a){var b,
+a="string"===typeof a?{name:a}:a;b=a.name;a.location&&(j.paths[b]=a.location);j.pkgs[b]=a.name+"/"+(a.main||"main").replace(ia,"").replace(Q,"")});B(h,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=p(b))});if(a.deps||a.callback)i.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(ba,arguments));return b||a.exports&&da(a.exports)}},makeRequire:function(a,e){function j(c,d,m){var n,q;e.enableBuildCallback&&(d&&G(d))&&(d.__requireJsBuild=
+!0);if("string"===typeof c){if(G(d))return w(C("requireargs","Invalid require call"),m);if(a&&t(L,c))return L[c](h[a.id]);if(g.get)return g.get(i,c,a,j);n=p(c,a,!1,!0);n=n.id;return!t(r,n)?w(C("notloaded",'Module name "'+n+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):r[n]}J();i.nextTick(function(){J();q=s(p(null,a));q.skipMap=e.skipMap;q.init(c,d,m,{enabled:!0});D()});return j}e=e||{};U(j,{isBrowser:z,toUrl:function(b){var d,e=b.lastIndexOf("."),k=b.split("/")[0];if(-1!==
+e&&(!("."===k||".."===k)||1<e))d=b.substring(e,b.length),b=b.substring(0,e);return i.nameToUrl(c(b,a&&a.id,!0),d,!0)},defined:function(b){return t(r,p(b,a,!1,!0).id)},specified:function(b){b=p(b,a,!1,!0).id;return t(r,b)||t(h,b)}});a||(j.undef=function(b){x();var c=p(b,a,!0),e=m(h,b);d(b);delete r[b];delete S[c.url];delete $[b];T(A,function(a,c){a[0]===b&&A.splice(c,1)});e&&(e.events.defined&&($[b]=e.events),y(b))});return j},enable:function(a){m(h,a.id)&&s(a).enable()},completeLoad:function(a){var b,
+c,d=m(j.shim,a)||{},g=d.exports;for(x();A.length;){c=A.shift();if(null===c[0]){c[0]=a;if(b)break;b=!0}else c[0]===a&&(b=!0);E(c)}c=m(h,a);if(!b&&!t(r,a)&&c&&!c.inited){if(j.enforceDefine&&(!g||!da(g)))return e(a)?void 0:w(C("nodefine","No define call for "+a,null,[a]));E([a,d.deps||[],d.exportsFn])}D()},nameToUrl:function(a,b,c){var d,e,h;(d=m(j.pkgs,a))&&(a=d);if(d=m(aa,a))return i.nameToUrl(d,b,c);if(g.jsExtRegExp.test(a))d=a+(b||"");else{d=j.paths;a=a.split("/");for(e=a.length;0<e;e-=1)if(h=a.slice(0,
+e).join("/"),h=m(d,h)){H(h)&&(h=h[0]);a.splice(0,e,h);break}d=a.join("/");d+=b||(/^data\:|\?/.test(d)||c?"":".js");d=("/"===d.charAt(0)||d.match(/^[\w\+\.\-]+:/)?"":j.baseUrl)+d}return j.urlArgs?d+((-1===d.indexOf("?")?"?":"&")+j.urlArgs):d},load:function(a,b){g.load(i,a,b)},execCb:function(a,b,c,d){return b.apply(d,c)},onScriptLoad:function(a){if("load"===a.type||ja.test((a.currentTarget||a.srcElement).readyState))N=null,a=I(a),i.completeLoad(a.id)},onScriptError:function(a){var b=I(a);if(!e(b.id))return w(C("scripterror",
+"Script error for: "+b.id,a,[b.id]))}};i.require=i.makeRequire();return i}var g,x,y,D,I,E,N,J,s,O,ka=/(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,la=/[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,Q=/\.js$/,ia=/^\.\//;x=Object.prototype;var K=x.toString,fa=x.hasOwnProperty,ha=Array.prototype.splice,z=!!("undefined"!==typeof window&&"undefined"!==typeof navigator&&window.document),ea=!z&&"undefined"!==typeof importScripts,ja=z&&"PLAYSTATION 3"===navigator.platform?/^complete$/:/^(complete|loaded)$/,
+Y="undefined"!==typeof opera&&"[object Opera]"===opera.toString(),F={},q={},R=[],M=!1;if("undefined"===typeof define){if("undefined"!==typeof requirejs){if(G(requirejs))return;q=requirejs;requirejs=void 0}"undefined"!==typeof require&&!G(require)&&(q=require,require=void 0);g=requirejs=function(b,c,d,e){var n,p="_";!H(b)&&"string"!==typeof b&&(n=b,H(c)?(b=c,c=d,d=e):b=[]);n&&n.context&&(p=n.context);(e=m(F,p))||(e=F[p]=g.s.newContext(p));n&&e.configure(n);return e.require(b,c,d)};g.config=function(b){return g(b)};
+g.nextTick="undefined"!==typeof setTimeout?function(b){setTimeout(b,4)}:function(b){b()};require||(require=g);g.version="2.1.15";g.jsExtRegExp=/^\/|:|\?|\.js$/;g.isBrowser=z;x=g.s={contexts:F,newContext:ga};g({});v(["toUrl","undef","defined","specified"],function(b){g[b]=function(){var c=F._;return c.require[b].apply(c,arguments)}});if(z&&(y=x.head=document.getElementsByTagName("head")[0],D=document.getElementsByTagName("base")[0]))y=x.head=D.parentNode;g.onError=ca;g.createNode=function(b){var c=
+b.xhtml?document.createElementNS("http://www.w3.org/1999/xhtml","html:script"):document.createElement("script");c.type=b.scriptType||"text/javascript";c.charset="utf-8";c.async=!0;return c};g.load=function(b,c,d){var e=b&&b.config||{};if(z)return e=g.createNode(e,c,d),e.setAttribute("data-requirecontext",b.contextName),e.setAttribute("data-requiremodule",c),e.attachEvent&&!(e.attachEvent.toString&&0>e.attachEvent.toString().indexOf("[native code"))&&!Y?(M=!0,e.attachEvent("onreadystatechange",b.onScriptLoad)):
+(e.addEventListener("load",b.onScriptLoad,!1),e.addEventListener("error",b.onScriptError,!1)),e.src=d,J=e,D?y.insertBefore(e,D):y.appendChild(e),J=null,e;if(ea)try{importScripts(d),b.completeLoad(c)}catch(m){b.onError(C("importscripts","importScripts failed for "+c+" at "+d,m,[c]))}};z&&!q.skipDataMain&&T(document.getElementsByTagName("script"),function(b){y||(y=b.parentNode);if(I=b.getAttribute("data-main"))return s=I,q.baseUrl||(E=s.split("/"),s=E.pop(),O=E.length?E.join("/")+"/":"./",q.baseUrl=
+O),s=s.replace(Q,""),g.jsExtRegExp.test(s)&&(s=I),q.deps=q.deps?q.deps.concat(s):[s],!0});define=function(b,c,d){var e,g;"string"!==typeof b&&(d=c,c=b,b=null);H(c)||(d=c,c=null);!c&&G(d)&&(c=[],d.length&&(d.toString().replace(ka,"").replace(la,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));if(M){if(!(e=J))N&&"interactive"===N.readyState||T(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return N=b}),e=N;e&&(b||
+(b=e.getAttribute("data-requiremodule")),g=F[e.getAttribute("data-requirecontext")])}(g?g.defQueue:R).push([b,c,d])};define.amd={jQuery:!0};g.exec=function(b){return eval(b)};g(q)}})(this);
index 843857b..9bb69b7 100644 (file)
     <version>5.2.1</version>
     <licenseversion>2.1</licenseversion>
   </library>
+  <library>
+    <location>requirejs</location>
+    <name>RequireJS</name>
+    <license>MIT</license>
+    <version>2.1.15</version>
+    <licenseversion></licenseversion>
+  </library>
 </libraries>
index d245d10..1a83ae9 100644 (file)
@@ -3,6 +3,7 @@ information provided here is intended especially for developers.
 
 === 2.9 ===
 
+* Support for loading AMD javascript modules has been added. See MDL-49046.
 * Webservice core_course_delete_courses now return warning messages on any failures and does not try to rollback the entire deletion.
 * \core\event\course_viewed 'other' argument renamed from coursesectionid to coursesectionnumber as it contains the section number.
 * New API core_filetypes::add_type (etc.) allows custom filetypes to be added and modified.
index 8fb28a6..ce30a8f 100644 (file)
@@ -685,7 +685,7 @@ class moodle_url {
     public function set_slashargument($path, $parameter = 'file', $supported = null) {
         global $CFG;
         if (is_null($supported)) {
-            $supported = $CFG->slasharguments;
+            $supported = !empty($CFG->slasharguments);
         }
 
         if ($supported) {
index 19a9eae..ee4d6ca 100644 (file)
@@ -64,6 +64,15 @@ if (!empty($user)) {
         }
     }
 
+    // Check whether the user should be changing password.
+    if (get_user_preferences('auth_forcepasswordchange', false, $user)) {
+        if ($userauth->can_change_password()) {
+            throw new moodle_exception('forcepasswordchangenotice');
+        } else {
+            throw new moodle_exception('nopasswordchangeforced', 'auth');
+        }
+    }
+
     // let enrol plugins deal with new enrolments if necessary
     enrol_check_plugins($user);
 
index d2041e2..81d6609 100644 (file)
@@ -181,12 +181,11 @@ function message_print_participants($context, $courseid, $contactselecturl=null,
     $pagingbar = new paging_bar($countparticipants, $page, MESSAGE_CONTACTS_PER_PAGE, $PAGE->url, 'page');
     echo $OUTPUT->render($pagingbar);
 
-    echo html_writer::start_tag('table', array('id' => 'message_participants', 'class' => 'boxaligncenter', 'cellspacing' => '2', 'cellpadding' => '0', 'border' => '0'));
+    echo html_writer::start_tag('div', array('id' => 'message_participants', 'class' => 'boxaligncenter'));
 
-    echo html_writer::start_tag('tr');
-    echo html_writer::tag('td', $titletodisplay, array('colspan' => 3, 'class' => 'heading'));
-    echo html_writer::end_tag('tr');
+    echo html_writer::tag('div' , $titletodisplay, array('class' => 'heading'));
 
+    $users = '';
     foreach ($participants as $participant) {
         if ($participant->id != $USER->id) {
 
@@ -205,11 +204,16 @@ function message_print_participants($context, $courseid, $contactselecturl=null,
             }
 
             $participant->messagecount = 0;//todo it would be nice if the course participant could report new messages
-            message_print_contactlist_user($participant, $iscontact, $isblocked, $contactselecturl, $showactionlinks, $user2);
+            $content = message_print_contactlist_user($participant, $iscontact, $isblocked,
+                $contactselecturl, $showactionlinks, $user2);
+            $users .= html_writer::tag('li', $content);
         }
     }
+    if (strlen($users) > 0) {
+        echo html_writer::tag('ul', $users, array('id' => 'message-courseparticipants', 'class' => 'message-contacts'));
+    }
 
-    echo html_writer::end_tag('table');
+    echo html_writer::end_tag('div');
 }
 
 /**
@@ -266,31 +270,31 @@ function message_get_blocked_users($user1=null, $user2=null) {
  * @return void
  */
 function message_print_blocked_users($blockedusers, $contactselecturl=null, $showactionlinks=true, $titletodisplay=null, $user2=null) {
-    global $DB, $USER;
+    global $OUTPUT;
 
     $countblocked = count($blockedusers);
 
-    echo html_writer::start_tag('table', array('id' => 'message_contacts', 'class' => 'boxaligncenter'));
+    echo html_writer::start_tag('div', array('id' => 'message_contacts', 'class' => 'boxaligncenter'));
 
     if (!empty($titletodisplay)) {
-        echo html_writer::start_tag('tr');
-        echo html_writer::tag('td', $titletodisplay, array('colspan' => 3, 'class' => 'heading'));
-        echo html_writer::end_tag('tr');
+        echo html_writer::tag('div', $titletodisplay, array('class' => 'heading'));
     }
 
     if ($countblocked) {
-        echo html_writer::start_tag('tr');
-        echo html_writer::tag('td', get_string('blockedusers', 'message', $countblocked), array('colspan' => 3, 'class' => 'heading'));
-        echo html_writer::end_tag('tr');
+        echo html_writer::tag('div', get_string('blockedusers', 'message', $countblocked), array('class' => 'heading'));
 
         $isuserblocked = true;
         $isusercontact = false;
+        $blockeduserslist = '';
         foreach ($blockedusers as $blockeduser) {
-            message_print_contactlist_user($blockeduser, $isusercontact, $isuserblocked, $contactselecturl, $showactionlinks, $user2);
+            $content = message_print_contactlist_user($blockeduser, $isusercontact, $isuserblocked,
+                $contactselecturl, $showactionlinks, $user2);
+            $blockeduserslist .= html_writer::tag('li', $content);
         }
+        echo html_writer::tag('ul', $blockeduserslist, array('id' => 'message-blockedusers', 'class' => 'message-contacts'));
     }
 
-    echo html_writer::end_tag('table');
+    echo html_writer::end_tag('div');
 }
 
 /**
@@ -417,59 +421,78 @@ function message_print_contacts($onlinecontacts, $offlinecontacts, $strangers, $
         echo html_writer::tag('div', get_string('contactlistempty', 'message'), array('class' => 'heading'));
     }
 
-    echo html_writer::start_tag('table', array('id' => 'message_contacts', 'class' => 'boxaligncenter'));
-
     if (!empty($titletodisplay)) {
-        message_print_heading($titletodisplay);
+        echo html_writer::tag('div', $titletodisplay, array('class' => 'heading'));
     }
 
     if($countonlinecontacts) {
         // Print out list of online contacts.
 
         if (empty($titletodisplay)) {
-            message_print_heading(get_string('onlinecontacts', 'message', $countonlinecontacts));
+            echo html_writer::tag('div',
+                get_string('onlinecontacts', 'message', $countonlinecontacts),
+                array('class' => 'heading'));
         }
 
         $isuserblocked = false;
         $isusercontact = true;
+        $contacts = '';
         foreach ($onlinecontacts as $contact) {
             if ($minmessages == 0 || $contact->messagecount >= $minmessages) {
-                message_print_contactlist_user($contact, $isusercontact, $isuserblocked, $contactselecturl, $showactionlinks, $user2);
+                $content = message_print_contactlist_user($contact, $isusercontact, $isuserblocked,
+                    $contactselecturl, $showactionlinks, $user2);
+                $contacts .= html_writer::tag('li', $content);
             }
         }
+        if (strlen($contacts) > 0) {
+            echo html_writer::tag('ul', $contacts, array('id' => 'message-onlinecontacts', 'class' => 'message-contacts'));
+        }
     }
 
     if ($countofflinecontacts) {
         // Print out list of offline contacts.
 
         if (empty($titletodisplay)) {
-            message_print_heading(get_string('offlinecontacts', 'message', $countofflinecontacts));
+            echo html_writer::tag('div',
+                get_string('offlinecontacts', 'message', $countofflinecontacts),
+                array('class' => 'heading'));
         }
 
         $isuserblocked = false;
         $isusercontact = true;
+        $contacts = '';
         foreach ($offlinecontacts as $contact) {
             if ($minmessages == 0 || $contact->messagecount >= $minmessages) {
-                message_print_contactlist_user($contact, $isusercontact, $isuserblocked, $contactselecturl, $showactionlinks, $user2);
+                $content = message_print_contactlist_user($contact, $isusercontact, $isuserblocked,
+                    $contactselecturl, $showactionlinks, $user2);
+                $contacts .= html_writer::tag('li', $content);
             }
         }
+        if (strlen($contacts) > 0) {
+            echo html_writer::tag('ul', $contacts, array('id' => 'message-offlinecontacts', 'class' => 'message-contacts'));
+        }
 
     }
 
     // Print out list of incoming contacts.
     if ($countstrangers) {
-        message_print_heading(get_string('incomingcontacts', 'message', $countstrangers));
+        echo html_writer::tag('div', get_string('incomingcontacts', 'message', $countstrangers), array('class' => 'heading'));
 
         $isuserblocked = false;
         $isusercontact = false;
+        $contacts = '';
         foreach ($strangers as $stranger) {
             if ($minmessages == 0 || $stranger->messagecount >= $minmessages) {
-                message_print_contactlist_user($stranger, $isusercontact, $isuserblocked, $contactselecturl, $showactionlinks, $user2);
+                $content = message_print_contactlist_user($stranger, $isusercontact, $isuserblocked,
+                    $contactselecturl, $showactionlinks, $user2);
+                $contacts .= html_writer::tag('li', $content);
             }
         }
-    }
+        if (strlen($contacts) > 0) {
+            echo html_writer::tag('ul', $contacts, array('id' => 'message-incommingcontacts', 'class' => 'message-contacts'));
+        }
 
-    echo html_writer::end_tag('table');
+    }
 
     if ($countstrangers && ($countonlinecontacts + $countofflinecontacts == 0)) {  // Extra help
         echo html_writer::tag('div','('.get_string('addsomecontactsincoming', 'message').')',array('class' => 'note'));
@@ -708,32 +731,41 @@ function message_get_recent_conversations($user, $limitfrom=0, $limitto=100) {
     // There is a separate query for read and unread messages as they are stored
     // in different tables. They were originally retrieved in one query but it
     // was so large that it was difficult to be confident in its correctness.
-    $sql = "SELECT $userfields,
+    $uniquefield = $DB->sql_concat('message.useridfrom', "'-'", 'message.useridto');
+    $sql = "SELECT $uniquefield, $userfields,
                    message.id as mid, message.notification, message.smallmessage, message.fullmessage,
                    message.fullmessagehtml, message.fullmessageformat, message.timecreated,
                    contact.id as contactlistid, contact.blocked
-
               FROM {message_read} message
-              JOIN {user} otheruser ON otheruser.id = CASE
-                                WHEN message.useridto = :userid1 THEN message.useridfrom
-                                                                 ELSE message.useridto END
-         LEFT JOIN {message_contacts} contact ON contact.userid = :userid2 AND contact.contactid = otheruser.id
-
-             WHERE otheruser.deleted = 0
-               AND (message.useridto = :userid3 OR message.useridfrom = :userid4)
-               AND message.notification = 0
-               AND NOT EXISTS (
-                        SELECT 1
-                          FROM {message_read} othermessage
-                         WHERE ((othermessage.useridto = :userid5 AND othermessage.useridfrom = otheruser.id) OR
-                                (othermessage.useridfrom = :userid6 AND othermessage.useridto = otheruser.id))
-                           AND (othermessage.timecreated > message.timecreated OR (
-                                othermessage.timecreated = message.timecreated AND othermessage.id > message.id))
-                   )
-
+              JOIN (
+                        SELECT MAX(id) AS messageid,
+                               matchedmessage.useridto,
+                               matchedmessage.useridfrom
+                         FROM {message_read} matchedmessage
+                   INNER JOIN (
+                               SELECT MAX(recentmessages.timecreated) timecreated,
+                                      recentmessages.useridfrom,
+                                      recentmessages.useridto
+                                 FROM {message_read} recentmessages
+                                WHERE (recentmessages.useridfrom = :userid1 OR recentmessages.useridto = :userid2)
+                             GROUP BY recentmessages.useridfrom, recentmessages.useridto
+                              ) recent ON matchedmessage.useridto     = recent.useridto
+                           AND matchedmessage.useridfrom   = recent.useridfrom
+                           AND matchedmessage.timecreated  = recent.timecreated
+                      GROUP BY matchedmessage.useridto, matchedmessage.useridfrom
+                   ) messagesubset ON messagesubset.messageid = message.id
+              JOIN {user} otheruser ON (message.useridfrom = :userid4 AND message.useridto = otheruser.id)
+                OR (message.useridto   = :userid5 AND message.useridfrom   = otheruser.id)
+         LEFT JOIN {message_contacts} contact ON contact.userid  = :userid3 AND contact.userid = otheruser.id
+             WHERE otheruser.deleted = 0 AND message.notification = 0
           ORDER BY message.timecreated DESC";
-    $params = array('userid1' => $user->id, 'userid2' => $user->id, 'userid3' => $user->id,
-            'userid4' => $user->id, 'userid5' => $user->id, 'userid6' => $user->id);
+    $params = array(
+            'userid1' => $user->id,
+            'userid2' => $user->id,
+            'userid3' => $user->id,
+            'userid4' => $user->id,
+            'userid5' => $user->id,
+        );
     $read = $DB->get_records_sql($sql, $params, $limitfrom, $limitto);
 
     // We want to get the messages that have not been read. These are stored in the 'message' table. It is the
@@ -742,16 +774,23 @@ function message_get_recent_conversations($user, $limitfrom=0, $limitto=100) {
     $sql = str_replace('{message_read}', '{message}', $sql);
     $unread = $DB->get_records_sql($sql, $params, $limitfrom, $limitto);
 
-    $conversations = array();
-
     // Union the 2 result sets together looking for the message with the most
     // recent timecreated for each other user.
     // $conversation->id (the array key) is the other user's ID.
     $conversation_arrays = array($unread, $read);
     foreach ($conversation_arrays as $conversation_array) {
         foreach ($conversation_array as $conversation) {
-            if (empty($conversations[$conversation->id]) || $conversations[$conversation->id]->timecreated < $conversation->timecreated ) {
+            if (!isset($conversations[$conversation->id])) {
                 $conversations[$conversation->id] = $conversation;
+            } else {
+                $current = $conversations[$conversation->id];
+                if ($current->timecreated < $conversation->timecreated) {
+                    $conversations[$conversation->id] = $conversation;
+                } else if ($current->timecreated == $conversation->timecreated) {
+                    if ($current->mid < $conversation->mid) {
+                        $conversations[$conversation->id] = $conversation;
+                    }
+                }
             }
         }
     }
@@ -2006,7 +2045,7 @@ function message_print_message_history($user1, $user2 ,$search = '', $messagelim
 
         $strcontact = message_get_contact_add_remove_link($user2->iscontact, $user2->isblocked, $user2, $script, $text, $icon);
         $strblock   = message_get_contact_block_link($user2->iscontact, $user2->isblocked, $user2, $script, $text, $icon);
-        $useractionlinks = $strcontact.'&nbsp;|'.$strblock;
+        $useractionlinks = $strcontact.'&nbsp;|&nbsp;'.$strblock;
 
         echo html_writer::tag('div', $useractionlinks, array('class' => 'useractionlinks'));
     }
@@ -2195,6 +2234,7 @@ function message_print_contactlist_user($contact, $incontactlist = true, $isbloc
     global $OUTPUT, $USER, $COURSE;
     $fullname  = fullname($contact);
     $fullnamelink  = $fullname;
+    $output = '';
 
     $linkclass = '';
     if (!empty($selecteduser) && $contact->id == $selecteduser->id) {
@@ -2217,12 +2257,9 @@ function message_print_contactlist_user($contact, $incontactlist = true, $isbloc
         $strhistory = message_history_link($USER->id, $contact->id, true, '', '', 'icon');
     }
 
-    echo html_writer::start_tag('tr');
-    echo html_writer::start_tag('td', array('class' => 'pix'));
-    echo $OUTPUT->user_picture($contact, array('size' => 20, 'courseid' => $COURSE->id));
-    echo html_writer::end_tag('td');
-
-    echo html_writer::start_tag('td', array('class' => 'contact'));
+    $output .= html_writer::start_tag('div', array('class' => 'pix'));
+    $output .= $OUTPUT->user_picture($contact, array('size' => 20, 'courseid' => $COURSE->id));
+    $output .= html_writer::end_tag('div');
 
     $popupoptions = array(
             'height' => MESSAGE_DISCUSSION_HEIGHT,
@@ -2241,13 +2278,23 @@ function message_print_contactlist_user($contact, $incontactlist = true, $isbloc
         $link = new moodle_url("/message/index.php?id=$contact->id");
         $action = new popup_action('click', $link, "message_$contact->id", $popupoptions);
     }
-    echo $OUTPUT->action_link($link, $fullnamelink, $action, array('class' => $linkclass,'title' => get_string('sendmessageto', 'message', $fullname)));
 
-    echo html_writer::end_tag('td');
 
-    echo html_writer::tag('td', '&nbsp;'.$strcontact.$strblock.'&nbsp;'.$strhistory, array('class' => 'link'));
+    if (strlen($strcontact . $strblock . $strhistory) > 0) {
+        $output .= html_writer::tag('div', $strcontact . $strblock . $strhistory, array('class' => 'link'));
+
+        $output .= html_writer::start_tag('div', array('class' => 'contact'));
+        $linkattr = array('class' => $linkclass, 'title' => get_string('sendmessageto', 'message', $fullname));
+        $output .= $OUTPUT->action_link($link, $fullnamelink, $action, $linkattr);
+        $output .= html_writer::end_tag('div');
+    } else {
+        $output .= html_writer::start_tag('div', array('class' => 'contact nolinks'));
+        $linkattr = array('class' => $linkclass, 'title' => get_string('sendmessageto', 'message', $fullname));
+        $output .= $OUTPUT->action_link($link, $fullnamelink, $action, $linkattr);
+        $output .= html_writer::end_tag('div');
+    }
 
-    echo html_writer::end_tag('tr');
+    return $output;
 }
 
 /**
@@ -2294,9 +2341,9 @@ function message_get_contact_block_link($incontactlist, $isblocked, $contact, $s
         //$strblock = '';
     } else*/
     if ($isblocked) {
-        $strblock   = '&nbsp;'.message_contact_link($contact->id, 'unblock', true, $script, $text, $icon);
+        $strblock   = message_contact_link($contact->id, 'unblock', true, $script, $text, $icon);
     } else{
-        $strblock   = '&nbsp;'.message_contact_link($contact->id, 'block', true, $script, $text, $icon);
+        $strblock   = message_contact_link($contact->id, 'block', true, $script, $text, $icon);
     }
 
     return $strblock;
@@ -2387,19 +2434,6 @@ function message_mark_message_read($message, $timeread, $messageworkingempty=fal
     return $messagereadid;
 }
 
-/**
- * A helper function that prints a formatted heading
- *
- * @param string $title the heading to display
- * @param int $colspan
- * @return void
- */
-function message_print_heading($title, $colspan=3) {
-    echo html_writer::start_tag('tr');
-    echo html_writer::tag('td', $title, array('colspan' => $colspan, 'class' => 'heading'));
-    echo html_writer::end_tag('tr');
-}
-
 /**
  * Get all message processors, validate corresponding plugin existance and
  * system configuration
index de51944..801e5aa 100644 (file)
@@ -18,13 +18,13 @@ Feature: Manage contacts
     When I click on "Add contact" "link" in the "User Two" "table_row"
     Then I should see "Message 1 from user1 to user2"
     And I should see "Message 2 from user1 to user2"
-    And I should see "User Two" in the "#message_contacts" "css_element"
+    And I should see "User Two" in the ".contactselector" "css_element"
     And I follow "Remove contact"
-    And I should not see "User Two" in the "#message_contacts" "css_element"
+    And I should not see "User Two" in the ".contactselector" "css_element"
     And I should not see "Remove contact"
     And I should see "Add contact"
     And I follow "Add contact"
-    And I should see "User Two" in the "#message_contacts" "css_element"
+    And I should see "User Two" in the ".contactselector" "css_element"
 
   @javascript
   Scenario: Adding and removing contacts with Javascript enabled
index 57006d0..38fc3fc 100644 (file)
@@ -72,6 +72,7 @@ class core_message_messagelib_testcase extends advanced_testcase {
         $record->useridto = $userto->id;
         $record->subject = 'No subject';
         $record->fullmessage = $message;
+        $record->smallmessage = $message;
         $record->timecreated = time();
 
         return $DB->insert_record('message', $record);
@@ -362,68 +363,261 @@ class core_message_messagelib_testcase extends advanced_testcase {
     }
 
     /**
-     * Test message_get_recent_conversations.
+     * The data provider for message_get_recent_conversations.
+     *
+     * This provides sets of data to for testing.
+     * @return array
      */
-    public function test_message_get_recent_conversations() {
-        global $DB, $USER;
-
-        // Set this user as the admin.
-        $this->setAdminUser();
-
-        // Create user's to send messages to/from.
-        $user1 = $this->getDataGenerator()->create_user(array('firstname' => 'Test1', 'lastname' => 'user1'));
-        $user2 = $this->getDataGenerator()->create_user(array('firstname' => 'Test2', 'lastname' => 'user2'));
-
-        // Add a few messages that have been read and some that are unread.
-        $m1 = $this->send_fake_message($USER, $user1, 'Message 1'); // An unread message.
-        $m2 = $this->send_fake_message($user1, $USER, 'Message 2'); // An unread message.
-        $m3 = $this->send_fake_message($USER, $user1, 'Message 3'); // An unread message.
-        $m4 = message_post_message($USER, $user2, 'Message 4', FORMAT_PLAIN);
-        $m5 = message_post_message($user2, $USER, 'Message 5', FORMAT_PLAIN);
-        $m6 = message_post_message($USER, $user2, 'Message 6', FORMAT_PLAIN);
-
-        // We want to alter the timecreated values so we can ensure message_get_recent_conversations orders
-        // by timecreated, not the max id, to begin with. However, we also want more than one message to have
-        // the same timecreated value to ensure that when this happens we retrieve the one with the maximum id.
-
-        // Store the current time.
-        $time = time();
-
-        // Set the first and second unread messages to have the same timecreated value.
-        $updatemessage = new stdClass();
-        $updatemessage->id = $m1;
-        $updatemessage->timecreated = $time;
-        $DB->update_record('message', $updatemessage);
-
-        $updatemessage->id = $m2;
-        $updatemessage->timecreated = $time;
-        $DB->update_record('message', $updatemessage);
-
-        // Set the third unread message to have a timecreated value of 0.
-        $updatemessage->id = $m3;
-        $updatemessage->timecreated = 0;
-        $DB->update_record('message', $updatemessage);
-
-        // Set the first and second read messages to have the same timecreated value.
-        $updatemessage->id = $m4;
-        $updatemessage->timecreated = $time + 1;
-        $DB->update_record('message', $updatemessage);
-
-        $updatemessage->id = $m5;
-        $updatemessage->timecreated = $time + 1;
-        $DB->update_record('message', $updatemessage);
-
-        // Set the third read message to have a timecreated value of 0.
-        $updatemessage->id = $m6;
-        $updatemessage->timecreated = 0;
-        $DB->update_record('message_read', $updatemessage);
+    public function message_get_recent_conversations_provider() {
+        return array(
+            array(
+                // Test that conversations with multiple contacts is correctly ordered.
+                'users' => array(
+                    'user1',
+                    'user2',
+                    'user3',
+                ),
+                'messages' => array(
+                    array(
+                        'from'          => 'user1',
+                        'to'            => 'user2',
+                        'state'         => 'unread',
+                        'subject'       => 'S1',
+                    ),
+                    array(
+                        'from'          => 'user2',
+                        'to'            => 'user1',
+                        'state'         => 'unread',
+                        'subject'       => 'S2',
+                    ),
+                    array(
+                        'from'          => 'user1',
+                        'to'            => 'user2',
+                        'state'         => 'unread',
+                        'timecreated'   => 0,
+                        'subject'       => 'S3',
+                    ),
+                    array(
+                        'from'          => 'user1',
+                        'to'            => 'user3',
+                        'state'         => 'read',
+                        'timemodifier'  => 1,
+                        'subject'       => 'S4',
+                    ),
+                    array(
+                        'from'          => 'user3',
+                        'to'            => 'user1',
+                        'state'         => 'read',
+                        'timemodifier'  => 1,
+                        'subject'       => 'S5',
+                    ),
+                    array(
+                        'from'          => 'user1',
+                        'to'            => 'user3',
+                        'state'         => 'read',
+                        'timecreated'   => 0,
+                        'subject'       => 'S6',
+                    ),
+                ),
+                'expectations' => array(
+                    'user1' => array(
+                        // User1 has conversed most recently with user3. The most recent message is M5.
+                        array(
+                            'messageposition'   => 0,
+                            'with'              => 'user3',
+                            'subject'           => 'S5',
+                        ),
+                        // User1 has also conversed with user2. The most recent message is S2.
+                        array(
+                            'messageposition'   => 1,
+                            'with'              => 'user2',
+                            'subject'           => 'S2',
+                        ),
+                    ),
+                    'user2' => array(
+                        // User2 has only conversed with user1. Their most recent shared message was S2.
+                        array(
+                            'messageposition'   => 0,
+                            'with'              => 'user1',
+                            'subject'           => 'S2',
+                        ),
+                    ),
+                    'user3' => array(
+                        // User3 has only conversed with user1. Their most recent shared message was S5.
+                        array(
+                            'messageposition'   => 0,
+                            'with'              => 'user1',
+                            'subject'           => 'S5',
+                        ),
+                    ),
+                ),
+            ),
+            array(
+                // Test conversations with a single user, where some messages are read and some are not.
+                'users' => array(
+                    'user1',
+                    'user2',
+                ),
+                'messages' => array(
+                    array(
+                        'from'          => 'user1',
+                        'to'            => 'user2',
+                        'state'         => 'read',
+                        'subject'       => 'S1',
+                    ),
+                    array(
+                        'from'          => 'user2',
+                        'to'            => 'user1',
+                        'state'         => 'read',
+                        'subject'       => 'S2',
+                    ),
+                    array(
+                        'from'          => 'user1',
+                        'to'            => 'user2',
+                        'state'         => 'unread',
+                        'timemodifier'  => 1,
+                        'subject'       => 'S3',
+                    ),
+                    array(
+                        'from'          => 'user1',
+                        'to'            => 'user2',
+                        'state'         => 'unread',
+                        'timemodifier'  => 1,
+                        'subject'       => 'S4',
+                    ),
+                ),
+                'expectations' => array(
+                    // The most recent message between user1 and user2 was S4.
+                    'user1' => array(
+                        array(
+                            'messageposition'   => 0,
+                            'with'              => 'user2',
+                            'subject'           => 'S4',
+                        ),
+                    ),
+                    'user2' => array(
+                        // The most recent message between user1 and user2 was S4.
+                        array(
+                            'messageposition'   => 0,
+                            'with'              => 'user1',
+                            'subject'           => 'S4',
+                        ),
+                    ),
+                ),
+            ),
+            array(
+                // Test conversations with a single user, where some messages are read and some are not, and messages
+                // are out of order.
+                // This can happen through a combination of factors including multi-master DB replication with messages
+                // read somehow (e.g. API).
+                'users' => array(
+                    'user1',
+                    'user2',
+                ),
+                'messages' => array(
+                    array(
+                        'from'          => 'user1',
+                        'to'            => 'user2',
+                        'state'         => 'read',
+                        'subject'       => 'S1',
+                        'timemodifier'  => 1,
+                    ),
+                    array(
+                        'from'          => 'user2',
+                        'to'            => 'user1',
+                        'state'         => 'read',
+                        'subject'       => 'S2',
+                        'timemodifier'  => 2,
+                    ),
+                    array(
+                        'from'          => 'user1',
+                        'to'            => 'user2',
+                        'state'         => 'unread',
+                        'subject'       => 'S3',
+                    ),
+                    array(
+                        'from'          => 'user1',
+                        'to'            => 'user2',
+                        'state'         => 'unread',
+                        'subject'       => 'S4',
+                    ),
+                ),
+                'expectations' => array(
+                    // The most recent message between user1 and user2 was S2, even though later IDs have not been read.
+                    'user1' => array(
+                        array(
+                            'messageposition'   => 0,
+                            'with'              => 'user2',
+                            'subject'           => 'S2',
+                        ),
+                    ),
+                    'user2' => array(
+                        array(
+                            'messageposition'   => 0,
+                            'with'              => 'user1',
+                            'subject'           => 'S2',
+                        ),
+                    ),
+                ),
+            ),
+        );
+    }
 
-        // Get the recent conversations for the current user.
-        $conversations = message_get_recent_conversations($USER);
+    /**
+     * Test message_get_recent_conversations with a mixture of messages.
+     *
+     * @dataProvider message_get_recent_conversations_provider
+     * @param array $usersdata The list of users to create for this test.
+     * @param array $messagesdata The list of messages to create.
+     * @param array $expectations The list of expected outcomes.
+     */
+    public function test_message_get_recent_conversations($usersdata, $messagesdata, $expectations) {
+        global $DB;
 
-        // Confirm that we have received the messages with the maximum timecreated, rather than the max id.
-        $this->assertEquals('Message 2', $conversations[0]->fullmessage);
-        $this->assertEquals('Message 5', $conversations[1]->smallmessage);
+        // Create all of the users.
+        $users = array();
+        foreach ($usersdata as $username) {
+            $users[$username] = $this->getDataGenerator()->create_user(array('username' => $username));
+        }
+
+        $defaulttimecreated = time();
+        foreach ($messagesdata as $messagedata) {
+            $from       = $users[$messagedata['from']];
+            $to         = $users[$messagedata['to']];
+            $subject    = $messagedata['subject'];
+
+            if (isset($messagedata['state']) && $messagedata['state'] == 'unread') {
+                $table = 'message';
+                $messageid = $this->send_fake_message($from, $to, $subject, FORMAT_PLAIN);
+            } else {
+                // If there is no state, or the state is not 'unread', assume the message is read.
+                $table = 'message_read';
+                $messageid = message_post_message($from, $to, $subject, FORMAT_PLAIN);
+            }
+
+            $updatemessage = new stdClass();
+            $updatemessage->id = $messageid;
+            if (isset($messagedata['timecreated'])) {
+                $updatemessage->timecreated = $messagedata['timecreated'];
+            } else if (isset($messagedata['timemodifier'])) {
+                $updatemessage->timecreated = $defaulttimecreated +&nbs