Merge branch 'MDL-52895-master' of git://github.com/jleyva/moodle
authorDan Poltawski <dan@moodle.com>
Tue, 9 Feb 2016 09:43:21 +0000 (09:43 +0000)
committerDan Poltawski <dan@moodle.com>
Tue, 9 Feb 2016 09:43:21 +0000 (09:43 +0000)
134 files changed:
Gruntfile.js
auth/ldap/auth.php
auth/mnet/land.php
backup/backup.php
backup/restore.php
blocks/activity_results/tests/behat/addunsupportedactivity.feature
blocks/activity_results/tests/behat/highscoreswithscales.feature
blocks/activity_results/tests/behat/highscoreswithscalesandgroups.feature
blocks/activity_results/tests/behat/lowscoreswithscales.feature
blocks/activity_results/tests/behat/lowscoreswithscalesandgroups.feature
blocks/blog_tags/block_blog_tags.php
blocks/blog_tags/tests/behat/blogtag.feature
blocks/glossary_random/backup/moodle2/restore_glossary_random_block_task.class.php
blocks/tags/block_tags.php
blocks/tags/edit_form.php
blocks/tags/lang/en/block_tags.php
blocks/tags/tests/behat/tagcloud.feature
blog/tests/lib_test.php
course/tests/behat/coursetags.feature
enrol/ldap/lib.php
enrol/ldap/tests/ldap_test.php
filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-debug.js
filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-min.js
filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker.js
filter/glossary/yui/src/autolinker/js/autolinker.js
install/lang/dk_kursus/langconfig.php [new file with mode: 0644]
lang/en/deprecated.txt
lang/en/role.php
lang/en/tag.php
lib/amd/build/fragment.min.js [new file with mode: 0644]
lib/amd/build/tag.min.js
lib/amd/src/fragment.js [new file with mode: 0644]
lib/amd/src/tag.js
lib/behat/behat_base.php
lib/classes/task/delete_incomplete_users_task.php
lib/db/install.xml
lib/db/services.php
lib/db/tag.php
lib/db/upgrade.php
lib/deprecatedlib.php
lib/excellib.class.php
lib/external/externallib.php
lib/filelib.php
lib/form/modgrade.php
lib/form/tags.php
lib/ldaplib.php
lib/outputfragmentrequirementslib.php [new file with mode: 0644]
lib/pagelib.php
lib/phpmailer/moodle_phpmailer.php
lib/phpunit/classes/util.php
lib/testing/generator/data_generator.php
lib/tests/behat/behat_hooks.php
lib/tests/formslib_test.php
lib/tests/ldaplib_test.php
login/signup_form.php
login/token.php
message/tests/behat/delete_messages.feature
mod/assign/gradingtable.php
mod/assign/locallib.php
mod/assign/tests/behat/allow_another_attempt.feature
mod/assign/tests/behat/edit_previous_feedback.feature
mod/assign/tests/events_test.php
mod/assign/tests/locallib_test.php
mod/assign/yui/build/moodle-mod_assign-history/moodle-mod_assign-history-debug.js
mod/assign/yui/build/moodle-mod_assign-history/moodle-mod_assign-history-min.js
mod/assign/yui/build/moodle-mod_assign-history/moodle-mod_assign-history.js
mod/assign/yui/src/history/js/history.js
mod/choice/classes/event/answer_deleted.php [new file with mode: 0644]
mod/choice/classes/event/report_downloaded.php [new file with mode: 0644]
mod/choice/lang/en/choice.php
mod/choice/lib.php
mod/choice/report.php
mod/choice/tests/events_test.php
mod/choice/version.php
mod/choice/view.php
mod/folder/db/install.xml
mod/folder/db/upgrade.php
mod/folder/download_folder.php [new file with mode: 0644]
mod/folder/lang/en/folder.php
mod/folder/lib.php
mod/folder/mod_form.php
mod/folder/renderer.php
mod/folder/settings.php
mod/folder/version.php
mod/forum/db/messages.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/templates/forum_post_email_htmlemail.mustache
mod/forum/templates/forum_post_email_htmlemail_body.mustache [new file with mode: 0644]
mod/forum/templates/forum_post_emaildigestfull_htmlemail.mustache
mod/forum/tests/maildigest_test.php
mod/forum/version.php
mod/survey/report.php
mod/wiki/tests/behat/edit_tags.feature
npm-shrinkwrap.json
package.json
question/engine/datalib.php
question/tests/previewlib_test.php [new file with mode: 0644]
question/type/ddimageortext/styles.css
question/type/ddwtos/styles.css
tag/classes/area.php
tag/classes/areas_table.php
tag/classes/collection.php
tag/classes/external.php
tag/classes/manage_table.php
tag/classes/output/tag.php
tag/classes/output/tagcloud.php
tag/classes/output/taglist.php
tag/classes/renderer.php
tag/classes/tag.php
tag/edit.php
tag/edit_form.php
tag/manage.php
tag/templates/tagcloud.mustache
tag/templates/tagisstandard.mustache [moved from tag/templates/tagtype.mustache with 66% similarity]
tag/templates/taglist.mustache
tag/tests/behat/collections.feature
tag/tests/behat/delete_tag.feature
tag/tests/behat/edit_tag.feature
tag/tests/behat/flag_tags.feature
tag/tests/behat/official_tags.feature [deleted file]
tag/tests/behat/standard_tags.feature [new file with mode: 0644]
tag/tests/external_test.php
tag/tests/taglib_test.php
tag/upgrade.txt
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/style/moodle.css
user/edit_form.php
user/editadvanced_form.php
user/editlib.php
user/forum.php
user/lib.php
user/tests/behat/name_fields.feature [new file with mode: 0644]
version.php

index 3dd3ef0..7412970 100644 (file)
@@ -12,6 +12,7 @@
 //
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+/* jshint node: true, browser: false */
 
 /**
  * @copyright  2014 Andrew Nicols
 
 module.exports = function(grunt) {
     var path = require('path'),
-        fs = require('fs'),
         tasks = {},
-        cwd = process.env.PWD || process.cwd(),
-        inAMD = path.basename(cwd) == 'amd';
+        cwd = process.env.PWD || process.cwd();
+
+    // Windows users can't run grunt in a subdirectory, so allow them to set
+    // the root by passing --root=path/to/dir.
+    if (grunt.option('root')) {
+        var root = grunt.option('root');
+        if (grunt.file.exists(__dirname, root)) {
+            cwd = path.join(__dirname, root);
+            grunt.log.ok('Setting root to '+cwd);
+        } else {
+            grunt.fail.fatal('Setting root to '+root+' failed - path does not exist');
+        }
+    }
+
+    var inAMD = path.basename(cwd) == 'amd';
+
+    // Globbing pattern for matching all AMD JS source files.
+    var amdSrc = [inAMD ? cwd + '/src/*.js' : '**/amd/src/*.js'];
+
+    /**
+     * Function to generate the destination for the uglify task
+     * (e.g. build/file.min.js). This function will be passed to
+     * the rename property of files array when building dynamically:
+     * http://gruntjs.com/configuring-tasks#building-the-files-object-dynamically
+     *
+     * @param {String} destPath the current destination
+     * @param {String} srcPath the  matched src path
+     * @return {String} The rewritten destination path.
+     */
+    var uglify_rename = function (destPath, srcPath) {
+        destPath = srcPath.replace('src', 'build');
+        destPath = destPath.replace('.js', '.min.js');
+        destPath = path.resolve(cwd, destPath);
+        return destPath;
+    };
 
     // Project configuration.
     grunt.initConfig({
         jshint: {
             options: {jshintrc: '.jshintrc'},
-            files: [inAMD ? cwd + '/src/*.js' : '**/amd/src/*.js']
+            amd: { src: amdSrc }
         },
         uglify: {
-            dynamic_mappings: {
-                files: grunt.file.expandMapping(
-                    ['**/src/*.js', '!**/node_modules/**'],
-                    '',
-                    {
-                        cwd: cwd,
-                        rename: function(destBase, destPath) {
-                            destPath = destPath.replace('src', 'build');
-                            destPath = destPath.replace('.js', '.min.js');
-                            destPath = path.resolve(cwd, destPath);
-                            return destPath;
-                        }
-                    }
-                )
+            amd: {
+                files: [{
+                    expand: true,
+                    src: amdSrc,
+                    rename: uglify_rename
+                }]
             }
         },
         less: {
@@ -62,54 +87,79 @@ module.exports = function(grunt) {
                     compress: true
                 }
            }
+        },
+        watch: {
+            options: {
+                nospawn: true // We need not to spawn so config can be changed dynamically.
+            },
+            amd: {
+                files: ['**/amd/src/**/*.js'],
+                tasks: ['amd']
+            },
+            bootstrapbase: {
+                files: ["theme/bootstrapbase/less/**/*.less"],
+                tasks: ["less:bootstrapbase"]
+            },
+            yui: {
+                files: ['**/yui/src/**/*.js'],
+                tasks: ['shifter']
+            },
+        },
+        shifter: {
+            options: {
+                recursive: true,
+                paths: [cwd]
+            }
         }
     });
 
+    /**
+     * Shifter task. Is configured with a path to a specific file or a directory,
+     * in the case of a specific file it will work out the right module to be built.
+     *
+     * Note that this task runs the invidiaul shifter jobs async (becase it spawns
+     * so be careful to to call done().
+     */
     tasks.shifter = function() {
-       var  exec = require('child_process').spawn,
+        var async = require('async'),
             done = this.async(),
-            args = [],
-            options = {
-                recursive: true,
-                watch: false,
-                walk: false,
-                module: false
-            },
-            shifter;
+            options = grunt.config('shifter.options');
 
+        // Run the shifter processes one at a time to avoid confusing output.
+        async.eachSeries(options.paths, function (src, filedone) {
+            var args = [];
             args.push( path.normalize(__dirname + '/node_modules/shifter/bin/shifter'));
 
+            // Always ignore the node_modules directory.
+            args.push('--excludes', 'node_modules');
+
             // Determine the most appropriate options to run with based upon the current location.
-            if (path.basename(cwd) === 'src') {
-                // Detect whether we're in a src directory.
+            if (grunt.file.isMatch('**/yui/**/*.js', src)) {
+                // When passed a JS file, build our containing module (this happen with
+                // watch).
+                grunt.log.debug('Shifter passed a specific JS file');
+                src = path.dirname(path.dirname(src));
+                options.recursive = false;
+            } else if (grunt.file.isMatch('**/yui/src', src)) {
+                // When in a src directory --walk all modules.
                 grunt.log.debug('In a src directory');
                 args.push('--walk');
-                options.walk = true;
-            } else if (path.basename(path.dirname(cwd)) === 'src') {
-                // Detect whether we're in a module directory.
+                options.recursive = false;
+            } else if (grunt.file.isMatch('**/yui/src/*', src)) {
+                // When in module, only build our module.
                 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');
+            } else if (grunt.file.isMatch('**/yui/src/*/js', src)) {
+                // When in module src, only build our module.
+                grunt.log.debug('In a source directory');
+                src = path.dirname(src);
+                options.recursive = false;
             }
 
-            if (options.recursive) {
-                args.push('--recursive');
+            if (grunt.option('watch')) {
+                grunt.fail.fatal('The --watch option has been removed, please use `grunt watch` instead');
             }
 
-            // 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');
@@ -121,19 +171,17 @@ module.exports = function(grunt) {
 
             var execShifter = function() {
 
-                shifter = exec("node", args, {
-                    cwd: cwd,
-                    stdio: 'inherit',
-                    env: process.env
-                });
-
-                // Tidy up after exec.
-                shifter.on('exit', function (code) {
+                grunt.log.ok("Running shifter on " + src);
+                grunt.util.spawn({
+                    cmd: "node",
+                    args: args,
+                    opts: {cwd: src, stdio: 'inherit', env: process.env}
+                }, function (error, result, code) {
                     if (code) {
                         grunt.fail.fatal('Shifter failed with code: ' + code);
                     } else {
                         grunt.log.ok('Shifter build complete.');
-                        done();
+                        filedone();
                     }
                 });
             };
@@ -143,79 +191,15 @@ module.exports = function(grunt) {
                 execShifter();
             } else {
                 // Check that there are yui modules otherwise shifter ends with exit code 1.
-                var found = false;
-                var hasYuiModules = function(directory, callback) {
-                    fs.readdir(directory, function(err, files) {
-                        if (err) {
-                            return callback(err, null);
-                        }
-
-                        // If we already found a match there is no need to continue scanning.
-                        if (found === true) {
-                            return;
-                        }
-
-                        // We need to track the number of files to know when we return a result.
-                        var pending = files.length;
-
-                        // We first check files, so if there is a match we don't need further
-                        // async calls and we just return a true.
-                        for (var i = 0; i < files.length; i++) {
-                            if (files[i] === 'yui') {
-                                return callback(null, true);
-                            }
-                        }
-
-                        // Iterate through subdirs if there were no matches.
-                        files.forEach(function (file) {
-
-                            var p = path.join(directory, file);
-                            stat = fs.statSync(p);
-                            if (!stat.isDirectory()) {
-                                pending--;
-                            } else {
-
-                                // We defer the pending-1 until we scan the whole dir and subdirs.
-                                hasYuiModules(p, function(err, result) {
-                                    if (err) {
-                                        return callback(err);
-                                    }
-
-                                    if (result === true) {
-                                        // Once we get a true we notify the caller.
-                                        found = true;
-                                        return callback(null, true);
-                                    }
-
-                                    pending--;
-                                    if (pending === 0) {
-                                        // Notify the caller that the whole dir has been scaned and there are no matches.
-                                        return callback(null, false);
-                                    }
-                                });
-                            }
-
-                            // No subdirs here, otherwise the return would be deferred until all subdirs are scanned.
-                            if (pending === 0) {
-                                return callback(null, false);
-                            }
-                        });
-                    });
-                };
-
-                hasYuiModules(cwd, function(err, result) {
-                    if (err) {
-                        grunt.fail.fatal(err.message);
-                    }
-
-                    if (result === true) {
-                        execShifter();
-                    } else {
-                        grunt.log.ok('No YUI modules to build.');
-                        done();
-                    }
-                });
+                if (grunt.file.expand({cwd: src}, '**/yui/src/**/*.js').length > 0) {
+                    args.push('--recursive');
+                    execShifter();
+                } else {
+                    grunt.log.ok('No YUI modules to build.');
+                    filedone();
+                }
             }
+        }, done);
     };
 
     tasks.startup = function() {
@@ -232,11 +216,28 @@ module.exports = function(grunt) {
         }
     };
 
+    // On watch, we dynamically modify config to build only affected files. This
+    // method is slightly complicated to deal with multiple changed files at once (copied
+    // from the grunt-contrib-watch readme).
+    var changedFiles = Object.create(null);
+    var onChange = grunt.util._.debounce(function() {
+          var files = Object.keys(changedFiles);
+          grunt.config('jshint.amd.src', files);
+          grunt.config('uglify.amd.files', [{ expand: true, src: files, rename: uglify_rename }]);
+          grunt.config('shifter.options.paths', files);
+          changedFiles = Object.create(null);
+    }, 200);
+
+    grunt.event.on('watch', function(action, filepath) {
+          changedFiles[filepath] = action;
+          onChange();
+    });
 
     // Register NPM tasks.
     grunt.loadNpmTasks('grunt-contrib-uglify');
     grunt.loadNpmTasks('grunt-contrib-jshint');
     grunt.loadNpmTasks('grunt-contrib-less');
+    grunt.loadNpmTasks('grunt-contrib-watch');
 
     // Register JS tasks.
     grunt.registerTask('shifter', 'Run Shifter against the current directory', tasks.shifter);
index 9d3a4ad..0739c59 100644 (file)
@@ -113,31 +113,7 @@ class auth_plugin_ldap extends auth_plugin_base {
         }
 
         // Hack prefix to objectclass
-        if (empty($this->config->objectclass)) {
-            // Can't send empty filter
-            $this->config->objectclass = '(objectClass=*)';
-        } else if (stripos($this->config->objectclass, 'objectClass=') === 0) {
-            // Value is 'objectClass=some-string-here', so just add ()
-            // around the value (filter _must_ have them).
-            $this->config->objectclass = '('.$this->config->objectclass.')';
-        } else if (strpos($this->config->objectclass, '(') !== 0) {
-            // Value is 'some-string-not-starting-with-left-parentheses',
-            // which is assumed to be the objectClass matching value.
-            // So build a valid filter with it.
-            $this->config->objectclass = '(objectClass='.$this->config->objectclass.')';
-        } else {
-            // There is an additional possible value
-            // '(some-string-here)', that can be used to specify any
-            // valid filter string, to select subsets of users based
-            // on any criteria. For example, we could select the users
-            // whose objectClass is 'user' and have the
-            // 'enabledMoodleUser' attribute, with something like:
-            //
-            //   (&(objectClass=user)(enabledMoodleUser=1))
-            //
-            // In this particular case we don't need to do anything,
-            // so leave $this->config->objectclass as is.
-        }
+        $this->config->objectclass = ldap_normalise_objectclass($this->config->objectclass);
     }
 
     /**
index 59adbf3..8be9cf3 100644 (file)
@@ -35,6 +35,7 @@ $wantsremoteurl = optional_param('remoteurl', false, PARAM_BOOL);
 $url = new moodle_url('/auth/mnet/jump.php', array('token'=>$token, 'idp'=>$remotewwwroot, 'wantsurl'=>$wantsurl));
 if ($wantsremoteurl !== false) $url->param('remoteurl', $wantsremoteurl);
 $PAGE->set_url($url);
+$PAGE->set_context(context_system::instance());
 
 $site = get_site();
 
index 688726d..4e8ba89 100644 (file)
@@ -138,7 +138,9 @@ if ($backup->get_stage() == backup_ui::STAGE_FINAL) {
     }
 
     // Get HTML from logger.
-    $loghtml = $logger->get_html();
+    if ($CFG->debugdisplay) {
+        $loghtml = $logger->get_html();
+    }
 
     // Hide the progress display and first backup step bar (the 'finished' step will show next).
     echo html_writer::end_div();
index 25a216c..ab97369 100644 (file)
@@ -110,7 +110,9 @@ if (!$restore->is_independent()) {
             // Do actual restore.
             $restore->execute();
             // Get HTML from logger.
-            $loghtml = $logger->get_html();
+            if ($CFG->debugdisplay) {
+                $loghtml = $logger->get_html();
+            }
             // Hide this section because we are now going to make the page show 'finished'.
             echo html_writer::end_div();
             echo html_writer::script('document.getElementById("executionprogress").style.display = "none";');
index 111c96f..012549e 100644 (file)
@@ -35,6 +35,6 @@ Feature: The activity results block displays student scores
     When I follow "Test assignment"
     And I click on "Edit settings" "link" in the "Administration" "block"
     And I set the following fields to these values:
-      | id_modgrade_type | None |
+      | id_grade_modgrade_type | None |
     And I press "Save and return to course"
     Then I should see "Error: the activity selected uses a grading method that is not supported by this block." in the "Activity results" "block"
index 1263382..b5e52f8 100644 (file)
@@ -39,8 +39,8 @@ Feature: The activity results block displays student scores as scales
       | Assignment name | Test assignment |
       | Description | Offline text |
       | assignsubmission_file_enabled | 0 |
-      | id_modgrade_type | Scale |
-      | id_modgrade_scale | My Scale |
+      | id_grade_modgrade_type | Scale |
+      | id_grade_modgrade_scale | My Scale |
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
     And I turn editing mode on
index 7856da5..d546fda 100644 (file)
@@ -56,8 +56,8 @@ Feature: The activity results block displays student scores as scales
       | Assignment name | Test assignment |
       | Description | Offline text |
       | assignsubmission_file_enabled | 0 |
-      | id_modgrade_type | Scale |
-      | id_modgrade_scale | My Scale |
+      | id_grade_modgrade_type | Scale |
+      | id_grade_modgrade_scale | My Scale |
       | Group mode | Separate groups |
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
index 149e0f6..00f31f6 100644 (file)
@@ -39,8 +39,8 @@ Feature: The activity results block displays student scores as scales
       | Assignment name | Test assignment |
       | Description | Offline text |
       | assignsubmission_file_enabled | 0 |
-      | id_modgrade_type | Scale |
-      | id_modgrade_scale | My Scale |
+      | id_grade_modgrade_type | Scale |
+      | id_grade_modgrade_scale | My Scale |
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
     And I turn editing mode on
index 9134339..be5ae33 100644 (file)
@@ -56,8 +56,8 @@ Feature: The activity results block displays student scores as scales
       | Assignment name | Test assignment |
       | Description | Offline text |
       | assignsubmission_file_enabled | 0 |
-      | id_modgrade_type | Scale |
-      | id_modgrade_scale | My Scale |
+      | id_grade_modgrade_type | Scale |
+      | id_grade_modgrade_scale | My Scale |
       | Group mode | Separate groups |
     And I follow "Course 1"
     And I navigate to "Grades" node in "Course administration"
index 6edc7b5..f46bd41 100644 (file)
@@ -121,7 +121,7 @@ class block_blog_tags extends block_base {
             $type = " AND (p.publishstate = 'site' or p.publishstate='public')";
         }
 
-        $sql  = "SELECT t.id, t.tagtype, t.rawname, t.name, COUNT(DISTINCT ti.id) AS ct
+        $sql  = "SELECT t.id, t.isstandard, t.rawname, t.name, COUNT(DISTINCT ti.id) AS ct
                    FROM {tag} t, {tag_instance} ti, {post} p, {blog_association} ba
                   WHERE t.id = ti.tagid AND p.id = ti.itemid
                         $type
@@ -136,7 +136,7 @@ class block_blog_tags extends block_base {
         }
 
         $sql .= "
-               GROUP BY t.id, t.tagtype, t.name, t.rawname
+               GROUP BY t.id, t.isstandard, t.name, t.rawname
                ORDER BY ct DESC, t.name ASC";
 
         if ($tags = $DB->get_records_sql($sql, null, 0, $this->config->numberoftags)) {
@@ -165,7 +165,7 @@ class block_blog_tags extends block_base {
                     $size = 20 - ( (int)((($currenttag - 1) / $totaltags) * 20) );
                 }
 
-                $tag->class = "$tag->tagtype s$size";
+                $tag->class = ($tag->isstandard ? "standardtag " : "") . "s$size";
                 $etags[] = $tag;
 
             }
index 192ba5e..d840c38 100644 (file)
@@ -14,8 +14,8 @@ Feature: Adding blog tag block
       | fullname  | shortname |
       | Course 1  | c1        |
     And the following "tags" exist:
-      | name         | tagtype  |
-      | Neverusedtag | official |
+      | name         | isstandard  |
+      | Neverusedtag | 1           |
     And the following "course enrolments" exist:
       | user     | course | role           |
       | teacher1 | c1     | editingteacher |
index 017e407..cfc5bc1 100644 (file)
@@ -62,9 +62,16 @@ class restore_glossary_random_block_task extends restore_block_task {
             if (!empty($config->glossary)) {
                 // Get glossary mapping and replace it in config
                 if ($glossarymap = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'glossary', $config->glossary)) {
-                    $config->glossary = $glossarymap->newitemid;
+                    $mappedglossary = $DB->get_record('glossary', array('id' => $glossarymap->newitemid),
+                        'id,course,globalglossary', MUST_EXIST);
+                    $config->glossary = $mappedglossary->id;
+                    $config->courseid = $mappedglossary->course;
+                    $config->globalglossary = $mappedglossary->globalglossary;
                     $configdata = base64_encode(serialize($config));
                     $DB->set_field('block_instances', 'configdata', $configdata, array('id' => $blockid));
+                } else {
+                    // The block refers to a glossary not present in the backup file.
+                    $DB->set_field('block_instances', 'configdata', '', array('id' => $blockid));
                 }
             }
         }
index 31ced58..e483abe 100644 (file)
@@ -70,8 +70,8 @@ class block_tags extends block_base {
             $this->config->numberoftags = 80;
         }
 
-        if (empty($this->config->tagtype)) {
-            $this->config->tagtype = '';
+        if (empty($this->config->showstandard)) {
+            $this->config->showstandard = core_tag_tag::BOTH_STANDARD_AND_NOT;
         }
 
         if (empty($this->config->ctx)) {
@@ -102,7 +102,7 @@ class block_tags extends block_base {
         // Get a list of tags.
 
         $tagcloud = core_tag_collection::get_tag_cloud($this->config->tagcoll,
-                $this->config->tagtype,
+                $this->config->showstandard == core_tag_tag::STANDARD_ONLY,
                 $this->config->numberoftags,
                 'name', '', $this->page->context->id, $this->config->ctx, $this->config->rec);
         $this->content->text = $OUTPUT->render_from_template('core_tag/tagcloud', $tagcloud->export_for_template($OUTPUT));
index 89742c5..4ea8209 100644 (file)
@@ -48,10 +48,10 @@ class block_tags_edit_form extends block_edit_form {
         $mform->setDefault('config_numberoftags', 80);
 
         $defaults = array(
-            'official' => get_string('officialonly', 'block_tags'),
-            '' => get_string('anytype', 'block_tags'));
-        $mform->addElement('select', 'config_tagtype', get_string('defaultdisplay', 'block_tags'), $defaults);
-        $mform->setDefault('config_tagtype', '');
+            core_tag_tag::STANDARD_ONLY => get_string('standardonly', 'block_tags'),
+            core_tag_tag::BOTH_STANDARD_AND_NOT => get_string('anytype', 'block_tags'));
+        $mform->addElement('select', 'config_showstandard', get_string('defaultdisplay', 'block_tags'), $defaults);
+        $mform->setDefault('config_showstandard', core_tag_tag::BOTH_STANDARD_AND_NOT);
 
         $defaults = array(0 => context_system::instance()->get_context_name());
         $parentcontext = context::instance_by_id($this->block->instance->parentcontextid);
index 02dd7a1..8f1a1b7 100644 (file)
@@ -26,11 +26,11 @@ $string['anycollection'] = 'Any';
 $string['anytype'] = 'All';
 $string['configtitle'] = 'Block title';
 $string['disabledtags'] = 'Tags are disabled';
-$string['defaultdisplay'] = 'Tag type to display';
-$string['officialonly'] = 'Only official';
+$string['defaultdisplay'] = 'Display tags';
 $string['pluginname'] = 'Tags';
 $string['recursivecontext'] = 'Include child contexts';
 $string['recursivecontext_help'] = 'If unchecked, tags of items in the context specified above will be displayed excluding underlying contexts, for example, you can search on course level only without searching inside course activities';
+$string['standardonly'] = 'Only standard';
 $string['tagcollection'] = 'Tag collection';
 $string['tagcollection_help'] = 'Select tag collection to display tags from. If you choose "Any" '
         . 'the tags from all collections except for those marked with * will be displayed';
index 7a25490..36e1bc0 100644 (file)
@@ -13,8 +13,8 @@ Feature: Block tags displaying tag cloud
       | fullname  | shortname |
       | Course 1  | c1        |
     And the following "tags" exist:
-      | name         | tagtype  |
-      | Neverusedtag | official |
+      | name         | isstandard  |
+      | Neverusedtag | 1           |
     And the following "course enrolments" exist:
       | user     | course | role           |
       | teacher1 | c1     | editingteacher |
index 81eac30..80c3be5 100644 (file)
@@ -66,7 +66,7 @@ class core_blog_lib_testcase extends advanced_testcase {
 
         // Create default tag.
         $tag = $this->getDataGenerator()->create_tag(array('userid' => $user->id,
-            'rawname' => 'Testtagname', 'tagtype' => 'official'));
+            'rawname' => 'Testtagname', 'isstandard' => 1));
 
         // Create default post.
         $post = new stdClass();
index f76f93a..b330e80 100644 (file)
@@ -15,8 +15,8 @@ Feature: Tagging courses
       | Course 1  | c1        |
       | Course 2  | c2        |
     And the following "tags" exist:
-      | name         | tagtype  |
-      | Neverusedtag | official |
+      | name         | isstandard  |
+      | Neverusedtag | 1           |
     And the following "course enrolments" exist:
       | user     | course | role           |
       | teacher1 | c1     | editingteacher |
index ae1a9e4..8fa81d6 100644 (file)
@@ -33,6 +33,13 @@ class enrol_ldap_plugin extends enrol_plugin {
     protected $enroltype = 'enrol_ldap';
     protected $errorlogtag = '[ENROL LDAP] ';
 
+    /**
+     * The object class to use when finding users.
+     *
+     * @var string $userobjectclass
+     */
+    protected $userobjectclass;
+
     /**
      * Constructor for the plugin. In addition to calling the parent
      * constructor, we define and 'fix' some settings depending on the
@@ -59,8 +66,13 @@ class enrol_ldap_plugin extends enrol_plugin {
         unset($ldap_usertypes);
 
         $default = ldap_getdefaults();
-        // Remove the objectclass default, as the values specified there are for
-        // users, and we are dealing with groups here.
+
+        // The objectclass in the defaults is for a user.
+        // This will be required later, but enrol_ldap uses 'objectclass' for its group objectclass.
+        // Save the normalised user objectclass for later.
+        $this->userobjectclass = ldap_normalise_objectclass($default['objectclass'][$this->get_config('user_type')]);
+
+        // Remove the objectclass default, as the values specified there are for users, and we are dealing with groups here.
         unset($default['objectclass']);
 
         // Use defaults if values not given. Dont use this->get_config()
@@ -72,31 +84,19 @@ class enrol_ldap_plugin extends enrol_plugin {
             }
         }
 
+        // Normalise the objectclass used for groups.
         if (empty($this->config->objectclass)) {
-            // Can't send empty filter. Fix it for now and future occasions
-            $this->set_config('objectclass', '(objectClass=*)');
-        } else if (stripos($this->config->objectclass, 'objectClass=') === 0) {
-            // Value is 'objectClass=some-string-here', so just add ()
-            // around the value (filter _must_ have them).
-            // Fix it for now and future occasions
-            $this->set_config('objectclass', '('.$this->config->objectclass.')');
-        } else if (stripos($this->config->objectclass, '(') !== 0) {
-            // Value is 'some-string-not-starting-with-left-parentheses',
-            // which is assumed to be the objectClass matching value.
-            // So build a valid filter with it.
-            $this->set_config('objectclass', '(objectClass='.$this->config->objectclass.')');
+            // No objectclass set yet - set a default class.
+            $this->config->objectclass = ldap_normalise_objectclass(null, '*');
+            $this->set_config('objectclass', $this->config->objectclass);
         } else {
-            // There is an additional possible value
-            // '(some-string-here)', that can be used to specify any
-            // valid filter string, to select subsets of users based
-            // on any criteria. For example, we could select the users
-            // whose objectClass is 'user' and have the
-            // 'enabledMoodleUser' attribute, with something like:
-            //
-            //   (&(objectClass=user)(enabledMoodleUser=1))
-            //
-            // In this particular case we don't need to do anything,
-            // so leave $this->config->objectclass as is.
+            $objectclass = ldap_normalise_objectclass($this->config->objectclass);
+            if ($objectclass !== $this->config->objectclass) {
+                // The objectclass was changed during normalisation.
+                // Save it in config, and update the local copy of config.
+                $this->set_config('objectclass', $objectclass);
+                $this->config->objectclass = $objectclass;
+            }
         }
     }
 
@@ -490,7 +490,7 @@ class enrol_ldap_plugin extends enrol_plugin {
                                 // as the idnumber does not match their dn and we get dn's from membership.
                                 $memberidnumbers = array();
                                 foreach ($ldapmembers as $ldapmember) {
-                                    $result = ldap_read($this->ldapconnection, $ldapmember, '(objectClass=*)',
+                                    $result = ldap_read($this->ldapconnection, $ldapmember, $this->userobjectclass,
                                                         array($this->config->idnumber_attribute));
                                     $entry = ldap_first_entry($this->ldapconnection, $result);
                                     $values = ldap_get_values($this->ldapconnection, $entry, $this->config->idnumber_attribute);
@@ -838,10 +838,9 @@ class enrol_ldap_plugin extends enrol_plugin {
         require_once($CFG->libdir.'/ldaplib.php');
 
         $ldap_contexts = explode(';', $this->get_config('user_contexts'));
-        $ldap_defaults = ldap_getdefaults();
 
         return ldap_find_userdn($this->ldapconnection, $userid, $ldap_contexts,
-                                '(objectClass='.$ldap_defaults['objectclass'][$this->get_config('user_type')].')',
+                                $this->userobjectclass,
                                 $this->get_config('idnumber_attribute'), $this->get_config('user_search_sub'));
     }
 
index be471bd..9ac6277 100644 (file)
@@ -469,4 +469,64 @@ class enrol_ldap_testcase extends advanced_testcase {
             }
         }
     }
+
+    /**
+     * Test that normalisation of the use objectclass is completed successfully.
+     *
+     * @dataProvider objectclass_fetch_provider
+     * @param string $usertype The supported user type
+     * @param string $expected The expected filter value
+     */
+    public function test_objectclass_fetch($usertype, $expected) {
+        $this->resetAfterTest();
+        // Set the user type - this must be performed before the plugin is instantiated.
+        set_config('user_type', $usertype, 'enrol_ldap');
+
+        // Fetch the plugin.
+        $instance = enrol_get_plugin('ldap');
+
+        // Use reflection to sneak a look at the plugin.
+        $rc = new ReflectionClass('enrol_ldap_plugin');
+        $rcp = $rc->getProperty('userobjectclass');
+        $rcp->setAccessible(true);
+
+        // Fetch the current userobjectclass value.
+        $value = $rcp->getValue($instance);
+        $this->assertEquals($expected, $value);
+    }
+
+    /**
+     * Data provider for the test_objectclass_fetch testcase.
+     *
+     * @return array of testcases.
+     */
+    public function objectclass_fetch_provider() {
+        return array(
+            // This is the list of values from ldap_getdefaults() normalised.
+            'edir' => array(
+                'edir',
+                '(objectClass=user)'
+            ),
+            'rfc2307' => array(
+                'rfc2307',
+                '(objectClass=posixaccount)'
+            ),
+            'rfc2307bis' => array(
+                'rfc2307bis',
+                '(objectClass=posixaccount)'
+            ),
+            'samba' => array(
+                'samba',
+                '(objectClass=sambasamaccount)'
+            ),
+            'ad' => array(
+                'ad',
+                '(samaccounttype=805306368)'
+            ),
+            'default' => array(
+                'default',
+                '(objectClass=*)'
+            ),
+        );
+    }
 }
index ecffc3c..58f6b4c 100644 (file)
Binary files a/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-debug.js and b/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-debug.js differ
index 46b748d..c2fdc14 100644 (file)
Binary files a/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-min.js and b/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-min.js differ
index ecffc3c..58f6b4c 100644 (file)
Binary files a/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker.js and b/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker.js differ
index 3689b4a..2d0471e 100644 (file)
@@ -73,7 +73,6 @@ Y.extend(AUTOLINKER, Y.Base, {
             alertpanelid,
             definition,
             position;
-        var self = this;
         try {
             data = Y.JSON.parse(content);
             if (data.success){
@@ -89,7 +88,7 @@ Y.extend(AUTOLINKER, Y.Base, {
 
                     // Register alertpanel for stacking.
                     alertpanelid = '#moodle-dialogue-' + alertpanel.get('COUNT');
-                    alertpanel.on('complete', this._deletealertpanel(self.alertpanels, alertpanelid));
+                    alertpanel.on('complete', this._deletealertpanel, this, alertpanelid);
 
                     // We already have some windows opened, so set the right position...
                     if (!Y.Object.isEmpty(this.alertpanels)){
@@ -118,8 +117,8 @@ Y.extend(AUTOLINKER, Y.Base, {
         });
         return lastPosition;
     },
-    _deletealertpanel : function(alertpanels, alertpanelid) {
-        delete alertpanels[alertpanelid];
+    _deletealertpanel : function(ev, alertpanelid) {
+        delete this.alertpanels[alertpanelid];
     }
 }, {
     NAME : AUTOLINKERNAME,
diff --git a/install/lang/dk_kursus/langconfig.php b/install/lang/dk_kursus/langconfig.php
new file mode 100644 (file)
index 0000000..e22a9ae
--- /dev/null
@@ -0,0 +1,34 @@
+<?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/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['parentlanguage'] = 'dk';
+$string['thislanguage'] = 'Dansk (kursus)';
index 366e26b..4cb19ce 100644 (file)
@@ -21,3 +21,9 @@ withselectedtags,core_tag
 tag:create,core_role
 categoriesanditems,core_grades
 taggedwith,core_tag
+officialtag,core_tag
+otags,core_tag
+othertags,core_tag
+tagtype,core_tag
+manageofficialtags,core_tag
+settypeofficial,core_tag
index f429e4c..e7eb45e 100644 (file)
@@ -86,8 +86,6 @@ $string['blog:associatemodule'] = 'This capability is deprecated and does nothin
 $string['blog:create'] = 'Create new blog entries';
 $string['blog:manageentries'] = 'Edit and manage entries';
 $string['blog:manageexternal'] = 'Edit and manage external blogs';
-$string['blog:manageofficialtags'] = 'Manage official tags';
-$string['blog:managepersonaltags'] = 'Manage personal tags';
 $string['blog:search'] = 'Search blog entries';
 $string['blog:view'] = 'View blog entries';
 $string['blog:viewdrafts'] = 'View draft blog entries';
index e20a603..5d576fd 100644 (file)
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['added'] = 'Official tag(s) added';
-$string['addotags'] = 'Add official tags';
+$string['added'] = 'Standard tag(s) added';
+$string['addotags'] = 'Add standard tags';
 $string['addtagcoll'] = 'Add tag collection';
 $string['addtagtomyinterests'] = 'Add "{$a}" to my interests';
 $string['alltagpages'] = 'All tag pages';
 $string['backtoallitems'] = 'Back to all items tagged with "{$a}"';
+$string['changeshowstandard'] = 'Change showing standard tags in area {$a}';
 $string['changessaved'] = 'Changes saved';
 $string['changetagcoll'] = 'Change tag collection of area {$a}';
 $string['collnameexplained'] = 'Leave the field empty to use the default value: {$a}';
@@ -69,7 +70,7 @@ $string['id'] = 'id';
 $string['inalltagcoll'] = 'Everywhere';
 $string['itemstaggedwith'] = '{$a->tagarea} tagged with "{$a->tag}"';
 $string['lesstags'] = 'less...';
-$string['manageofficialtags'] = 'Manage official tags';
+$string['managestandardtags'] = 'Manage standard tags';
 $string['managetags'] = 'Manage tags';
 $string['managetagcolls'] = 'Manage tag collections';
 $string['moretags'] = 'more...';
@@ -80,9 +81,6 @@ $string['nextpage'] = 'More';
 $string['notagsfound'] = 'No tags matching "{$a}" found';
 $string['noresultsfor'] = 'No results for "{$a}"';
 $string['nothingtoupdate'] = 'Nothing to update';
-$string['officialtag'] = 'Official';
-$string['otags'] = 'Official tags';
-$string['othertags'] = 'Other tags';
 $string['owner'] = 'Owner';
 $string['prevpage'] = 'Back';
 $string['ptags'] = 'User defined tags (Comma separated)';
@@ -94,6 +92,8 @@ $string['resetflag'] = 'Reset flag';
 $string['responsiblewillbenotified'] = 'The person responsible will be notified';
 $string['rssdesc'] = 'This RSS feed was automatically generated by Moodle and contains user generated tags for courses.';
 $string['rsstitle'] = 'Course tags RSS feed for user: {$a}';
+$string['showstandard'] = 'Standard tags usage';
+$string['showstandard_help'] = 'This controls how area handles standard tags. When user edits the item the standard tags may be suggested or not, it is also possible to force area to only use standard tags and not allow user to type new ones.';
 $string['search'] = 'Search';
 $string['searchable'] = 'Searchable';
 $string['searchable_help'] = 'Tags in this tag collection can be searched for on "Search tags" page. If unchecked, tags can still be accessed by clicking on them or via different search interfaces.';
@@ -103,9 +103,13 @@ $string['seeallblogs'] = 'See all blog entries tagged with "{$a}"';
 $string['select'] = 'Select';
 $string['selectcoll'] = 'Select tag collection';
 $string['selecttag'] = 'Select tag {$a}';
-$string['settypedefault'] = 'Remove from official tags';
-$string['settypeofficial'] = 'Make official';
+$string['settypedefault'] = 'Remove from standard tags';
+$string['settypestandard'] = 'Make standard';
 $string['showingfirsttags'] = 'Showing {$a} most popular tags';
+$string['standardforce'] = 'Force';
+$string['standardhide'] = 'Don\'t suggest';
+$string['standardsuggest'] = 'Suggest';
+$string['standardtag'] = 'Standard';
 $string['suredeletecoll'] = 'Are you sure you want to delete tag collection "{$a}"?';
 $string['tag'] = 'Tag';
 $string['tagarea_blog_external'] = 'External blog posts';
@@ -116,11 +120,11 @@ $string['tagareaenabled'] = 'Enabled';
 $string['tagareaname'] = 'Name';
 $string['tagareas'] = 'Tag areas';
 $string['tagcollection'] = 'Tag collection';
+$string['tagcollection_help'] = 'Tag collections are sets of tags for different areas. For example, a collection of standard tags can be used to tag courses, with user interests and blog post tags kept in a separate collection. When a user clicks on a tag, the tag page displays only items with that tag in the same collection. Tags can be automatically added to a collection according to the area tagged or can be added manually as standard tags.';
 $string['tagcollections'] = 'Tag collections';
 $string['tagdescription'] = 'Tag description';
 $string['tags'] = 'Tags';
 $string['tagsaredisabled'] = 'Tags are disabled';
-$string['tagtype'] = 'Tag type';
 $string['thingstaggedwith'] = '"{$a->name}" is used {$a->count} times';
 $string['thingtaggedwith'] = '"{$a->name}" is used once';
 $string['timemodified'] = 'Modified';
@@ -143,4 +147,10 @@ $string['withselectedtags'] = 'With selected tags...';
 
 // Deprecated since 3.1 .
 
+$string['manageofficialtags'] = 'Manage official tags';
+$string['officialtag'] = 'Official';
+$string['otags'] = 'Official tags';
+$string['othertags'] = 'Other tags';
+$string['settypeofficial'] = 'Make official';
 $string['taggedwith'] = 'tagged with "{$a}"';
+$string['tagtype'] = 'Tag type';
diff --git a/lib/amd/build/fragment.min.js b/lib/amd/build/fragment.min.js
new file mode 100644 (file)
index 0000000..89a6952
Binary files /dev/null and b/lib/amd/build/fragment.min.js differ
index 7a4328f..e570801 100644 (file)
Binary files a/lib/amd/build/tag.min.js and b/lib/amd/build/tag.min.js differ
diff --git a/lib/amd/src/fragment.js b/lib/amd/src/fragment.js
new file mode 100644 (file)
index 0000000..92d7e6d
--- /dev/null
@@ -0,0 +1,118 @@
+// 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/>.
+
+/**
+ * A way to call HTML fragments to be inserted as required via JavaScript.
+ *
+ * @module     core/fragment
+ * @class      fragment
+ * @package    core
+ * @copyright  2016 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      3.1
+ */
+define(['jquery', 'core/ajax', 'core/notification'], function($, ajax, notification) {
+
+    /**
+     * Loads an HTML fragment through a callback.
+     *
+     * @method loadFragment
+     * @param {string} component Component where callback is located.
+     * @param {string} callback Callback function name.
+     * @param {integer} contextid Context ID of the fragment.
+     * @param {object} params Parameters for the callback.
+     * @return {Promise} JQuery promise object resolved when the fragment has been loaded.
+     */
+    var loadFragment = function(component, callback, contextid, params) {
+        // Change params into required webservice format.
+        var formattedparams = [];
+        for (var index in params) {
+            formattedparams.push({name: index, value: params[index]});
+        }
+
+        // Ajax stuff.
+        var deferred = $.Deferred();
+
+        var promises = ajax.call([{
+            methodname: 'core_get_fragment',
+            args:{
+                component: component,
+                callback: callback,
+                contextid: contextid,
+                args: formattedparams
+            }
+        }], false);
+
+        promises[0].done(function(data) {
+            deferred.resolve(data);
+        }).fail(function(ex) {
+            deferred.reject(ex);
+        });
+        return deferred.promise();
+    };
+
+    /**
+     * Removes and cleans children of a node. This includes event handlers and listeners that may be
+     * attached to the nodes for both jquery and yui.
+     *
+     * @method recursiveCleanup
+     * @param {object} DOM node to be cleaned.
+     * @return {void}
+     */
+    var recursiveCleanup = function(node) {
+        node.children().each(function(index, el) {
+            var child = $(el);
+            recursiveCleanup(child);
+        });
+        var yuinode = new Y.Node(node);
+        node.empty();
+        node.remove();
+        yuinode.detachAll();
+        if (yuinode.get('childNodes')) {
+            yuinode.empty();
+        }
+        yuinode.remove(true);
+    };
+
+    return /** @alias module:core/fragment */{
+        /**
+         * Appends HTML and JavaScript fragments to specified nodes.
+         * Callbacks called by this AMD module are responsible for doing the appropriate security checks
+         * to access the information that is returned. This only does minimal validation on the context.
+         *
+         * @method fragmentAppend
+         * @param {string} component Component where callback is located.
+         * @param {string} callback Callback function name.
+         * @param {integer} contextid Context ID of the fragment.
+         * @param {object} params Parameters for the callback.
+         * @param {string} htmlnodeidentifier The 'class' or 'id' to attach the HTML.
+         * @param {string} javascriptnodeidentifier The 'class' or 'id' to attach the JavaScript.
+         * @return {void}
+         */
+        fragmentAppend: function(component, callback, contextid, params, htmlnodeidentifier, javascriptnodeidentifier) {
+            $.when(loadFragment(component, callback, contextid, params)).then(function(data) {
+                // Clean up previous code if found first.
+                recursiveCleanup($('#fragment-html'));
+                recursiveCleanup($('#fragment-scripts'));
+                // Attach new HTML and JavaScript.
+                $(htmlnodeidentifier).append('<div id="fragment-html">' + data.html + '</div>');
+                $(javascriptnodeidentifier).append('<div id="fragment-scripts">' + data.javascript + '</div>');
+
+            }).fail(function(ex) {
+                notification.exception(ex);
+            });
+        }
+    };
+});
index 01ef814..1a0a94e 100644 (file)
@@ -72,16 +72,16 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
             };
 
             // Click handler for changing tag type.
-            $('.tag-management-table').delegate('.tagtype', 'click', function(e) {
+            $('.tag-management-table').delegate('.tagisstandard', 'click', function(e) {
                 e.preventDefault();
                 var target = $( this ),
                     tagid = target.attr('data-id'),
                     currentvalue = target.attr('data-value'),
-                    official = (currentvalue === "1") ? 0 : 1;
+                    isstandard = (currentvalue === "1") ? 0 : 1;
 
                 var promises = ajax.call([{
                     methodname: 'core_tag_update_tags',
-                    args: { tags : [ { id : tagid , official : official } ] }
+                    args: { tags : [ { id : tagid , isstandard : isstandard } ] }
                 }, {
                     methodname: 'core_tag_get_tags',
                     args: { tags : [ { id : tagid } ] }
@@ -90,11 +90,11 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
                 $.when.apply($, promises)
                     .done( function(updateresult, data) {
                         if (updateresult.warnings[0] === undefined && data.tags[0] !== undefined) {
-                            templates.render('core_tag/tagtype', data.tags[0]).done(function(html) {
+                            templates.render('core_tag/tagisstandard', data.tags[0]).done(function(html) {
                                 update_modified(target);
                                 var parent = target.parent();
                                 target.replaceWith(html);
-                                parent.find('.tagtype').get(0).focus();
+                                parent.find('.tagisstandard').get(0).focus();
                             });
                         }
                     });
index c2dac9a..efe954b 100644 (file)
@@ -74,7 +74,7 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
     /**
      * The JS code to check that the page is ready.
      */
-    const PAGE_READY_JS = '(M && M.util && M.util.pending_js && !Boolean(M.util.pending_js.length)) && (document.readyState === "complete")';
+    const PAGE_READY_JS = '(typeof M !== "undefined" && M.util && M.util.pending_js && !Boolean(M.util.pending_js.length)) && (document.readyState === "complete")';
 
     /**
      * Locates url, based on provided path.
index 0fb84b8..dda16f7 100644 (file)
@@ -59,6 +59,10 @@ class delete_incomplete_users_task extends scheduled_task {
                 if (isguestuser($user) or is_siteadmin($user)) {
                     continue;
                 }
+                if ($user->lastname !== '' and $user->firstname !== '' and $user->email !== '') {
+                    // This can happen on MySQL - see MDL-52831.
+                    continue;
+                }
                 delete_user($user);
                 mtrace(" Deleted not fully setup user $user->username ($user->id)");
             }
index 3c9e420..c8cda52 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20160111" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20160202" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
         <FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="itemtype" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
-        <FIELD NAME="enabled" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
+        <FIELD NAME="enabled" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
         <FIELD NAME="tagcollid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="callback" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
         <FIELD NAME="callbackfile" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="showstandard" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
         <FIELD NAME="tagcollid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="rawname" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="The raw, unnormalised name for the tag as entered by users"/>
-        <FIELD NAME="tagtype" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="isstandard" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether this tag is standard"/>
         <FIELD NAME="description" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
         <FIELD NAME="descriptionformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="flag" TYPE="int" LENGTH="4" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="a tag can be 'flagged' as inappropriate"/>
       </KEYS>
       <INDEXES>
         <INDEX NAME="tagcollname" UNIQUE="true" FIELDS="tagcollid, name"/>
-        <INDEX NAME="tagcolltype" UNIQUE="false" FIELDS="tagcollid, tagtype"/>
+        <INDEX NAME="tagcolltype" UNIQUE="false" FIELDS="tagcollid, isstandard"/>
       </INDEXES>
     </TABLE>
     <TABLE NAME="tag_correlation" COMMENT="The rationale for the 'tag_correlation' table is performance.   It works as a cache for a potentially heavy load query done at the 'tag_instance' table.   So, the 'tag_correlation' table stores redundant information derived from the 'tag_instance' table">
index bc3230f..f422c5d 100644 (file)
@@ -1051,6 +1051,15 @@ $functions = array(
         'ajax'        => true,
     ),
 
+    'core_get_fragment' => array(
+        'classname'   => 'core_external',
+        'methodname'  => 'get_fragment',
+        'classpath'   => 'lib/external/externallib.php',
+        'description' => 'Return a fragment for inclusion, such as a JavaScript page.',
+        'type'        => 'read',
+        'ajax'        => true,
+    ),
+
 
     // === Calendar related functions ===
 
index 2923bb5..074b685 100644 (file)
@@ -28,6 +28,8 @@
  *     any other tag areas to this collection nor move this tag area elsewhere
  *   - searchable (only if collection is specified) - wether the tag collection
  *     should be searchable on /tag/search.php
+ *   - showstandard - default value for the "Standard tags" attribute of the area,
+ *     this is only respected when new tag area is added and ignored during upgrade
  *   - customurl (only if collection is specified) - custom url to use instead of
  *     /tag/search.php to display information about one tag
  *   - callback - name of the function that returns items tagged with this tag,
@@ -56,6 +58,7 @@ $tagareas = array(
         'component' => 'core',
         'callback' => 'user_get_tagged_users',
         'callbackfile' => '/user/lib.php',
+        'showstandard' => core_tag_tag::HIDE_STANDARD,
     ),
     array(
         'itemtype' => 'course', // Courses.
index 3975de3..7e6ca5c 100644 (file)
@@ -4849,5 +4849,72 @@ function xmldb_main_upgrade($oldversion) {
 
         upgrade_main_savepoint(true, 2016011901.00);
     }
+
+    if ($oldversion < 2016020200.00) {
+
+        // Define field isstandard to be added to tag.
+        $table = new xmldb_table('tag');
+        $field = new xmldb_field('isstandard', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'rawname');
+
+        // Conditionally launch add field isstandard.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Define index tagcolltype (not unique) to be dropped form tag.
+        $index = new xmldb_index('tagcolltype', XMLDB_INDEX_NOTUNIQUE, array('tagcollid', 'tagtype'));
+
+        // Conditionally launch drop index tagcolltype.
+        if ($dbman->index_exists($table, $index)) {
+            $dbman->drop_index($table, $index);
+        }
+
+        // Define index tagcolltype (not unique) to be added to tag.
+        $index = new xmldb_index('tagcolltype', XMLDB_INDEX_NOTUNIQUE, array('tagcollid', 'isstandard'));
+
+        // Conditionally launch add index tagcolltype.
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Define field tagtype to be dropped from tag.
+        $field = new xmldb_field('tagtype');
+
+        // Conditionally launch drop field tagtype and update isstandard.
+        if ($dbman->field_exists($table, $field)) {
+            $DB->execute("UPDATE {tag} SET isstandard=(CASE WHEN (tagtype = ?) THEN 1 ELSE 0 END)", array('official'));
+            $dbman->drop_field($table, $field);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2016020200.00);
+    }
+
+    if ($oldversion < 2016020201.00) {
+
+        // Define field showstandard to be added to tag_area.
+        $table = new xmldb_table('tag_area');
+        $field = new xmldb_field('showstandard', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'callbackfile');
+
+        // Conditionally launch add field showstandard.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // By default set user area to hide standard tags. 2 = core_tag_tag::HIDE_STANDARD (can not use constant here).
+        $DB->execute("UPDATE {tag_area} SET showstandard = ? WHERE itemtype = ? AND component = ?",
+            array(2, 'user', 'core'));
+
+        // Changing precision of field enabled on table tag_area to (1).
+        $table = new xmldb_table('tag_area');
+        $field = new xmldb_field('enabled', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '1', 'itemtype');
+
+        // Launch change of precision for field enabled.
+        $dbman->change_field_precision($table, $field);
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2016020201.00);
+    }
+
     return true;
 }
index 21c56f5..6222822 100644 (file)
@@ -2363,7 +2363,7 @@ function coursetag_get_tags($courseid, $userid=0, $tagtype='', $numtags=0, $unus
 
     // get tags from the db ordered by highest count first
     $params = array();
-    $sql = "SELECT id as tkey, name, id, tagtype, rawname, f.timemodified, flag, count
+    $sql = "SELECT id as tkey, name, id, isstandard, rawname, f.timemodified, flag, count
               FROM {tag} t,
                  (SELECT tagid, MAX(timemodified) as timemodified, COUNT(id) as count
                     FROM {tag_instance}
@@ -2388,8 +2388,8 @@ function coursetag_get_tags($courseid, $userid=0, $tagtype='', $numtags=0, $unus
     $sql .= "   GROUP BY tagid) f
              WHERE t.id = f.tagid ";
     if ($tagtype != '') {
-        $sql .= "AND tagtype = :tagtype ";
-        $params['tagtype'] = $tagtype;
+        $sql .= "AND isstandard = :isstandard ";
+        $params['isstandard'] = ($tagtype === 'official') ? 1 : 0;
     }
     $sql .= "ORDER BY count DESC, name ASC";
 
@@ -2430,7 +2430,7 @@ function coursetag_get_all_tags($unused='', $numtags=0) {
     global $CFG, $DB;
 
     // note that this selects all tags except for courses that are not visible
-    $sql = "SELECT id, name, tagtype, rawname, f.timemodified, flag, count
+    $sql = "SELECT id, name, isstandard, rawname, f.timemodified, flag, count
         FROM {tag} t,
         (SELECT tagid, MAX(timemodified) as timemodified, COUNT(id) as count
             FROM {tag_instance} WHERE tagid NOT IN
@@ -2628,7 +2628,7 @@ function coursetag_delete_course_tags($courseid, $showfeedback=false) {
 function tag_type_set($tagid, $type) {
     debugging('Function tag_type_set() is deprecated and can be replaced with use core_tag_tag::get($tagid)->update().', DEBUG_DEVELOPER);
     if ($tag = core_tag_tag::get($tagid, '*')) {
-        return $tag->update(array('tagtype' => $type));
+        return $tag->update(array('isstandard' => ($type === 'official') ? 1 : 0));
     }
     return false;
 }
@@ -2666,8 +2666,9 @@ function tag_description_set($tagid, $description, $descriptionformat) {
 function tag_get_tags($record_type, $record_id, $type=null, $userid=0) {
     debugging('Method tag_get_tags() is deprecated and replaced with core_tag_tag::get_item_tags(). ' .
         'Component is now required when retrieving tag instances.', DEBUG_DEVELOPER);
-    $official = ($type === 'official' ? true : (!empty($type) ? false : null));
-    $tags = core_tag_tag::get_item_tags(null, $record_type, $record_id, $official, $userid);
+    $standardonly = ($type === 'official' ? core_tag_tag::STANDARD_ONLY :
+        (!empty($type) ? core_tag_tag::NOT_STANDARD_ONLY : core_tag_tag::BOTH_STANDARD_AND_NOT));
+    $tags = core_tag_tag::get_item_tags(null, $record_type, $record_id, $standardonly, $userid);
     $rv = array();
     foreach ($tags as $id => $t) {
         $rv[$id] = $t->to_object();
@@ -2688,8 +2689,9 @@ function tag_get_tags($record_type, $record_id, $type=null, $userid=0) {
 function tag_get_tags_array($record_type, $record_id, $type=null) {
     debugging('Method tag_get_tags_array() is deprecated and replaced with core_tag_tag::get_item_tags_array(). ' .
         'Component is now required when retrieving tag instances.', DEBUG_DEVELOPER);
-    $official = ($type === 'official' ? true : (!empty($type) ? false : null));
-    return core_tag_tag::get_item_tags_array('', $record_type, $record_id, $official);
+    $standardonly = ($type === 'official' ? core_tag_tag::STANDARD_ONLY :
+        (!empty($type) ? core_tag_tag::NOT_STANDARD_ONLY : core_tag_tag::BOTH_STANDARD_AND_NOT));
+    return core_tag_tag::get_item_tags_array('', $record_type, $record_id, $standardonly);
 }
 
 /**
@@ -2710,11 +2712,12 @@ function tag_get_tags_csv($record_type, $record_id, $html=null, $type=null) {
     debugging('Method tag_get_tags_csv() is deprecated. Instead you should use either ' .
             'core_tag_tag::get_item_tags_array() or $OUTPUT->tag_list(core_tag_tag::get_item_tags()). ' .
         'Component is now required when retrieving tag instances.', DEBUG_DEVELOPER);
-    $official = ($type === 'official' ? true : (!empty($type) ? false : null));
+    $standardonly = ($type === 'official' ? core_tag_tag::STANDARD_ONLY :
+        (!empty($type) ? core_tag_tag::NOT_STANDARD_ONLY : core_tag_tag::BOTH_STANDARD_AND_NOT));
     if ($html != TAG_RETURN_TEXT) {
-        return $OUTPUT->tag_list(core_tag_tag::get_item_tags('', $record_type, $record_id, $official), '');
+        return $OUTPUT->tag_list(core_tag_tag::get_item_tags('', $record_type, $record_id, $standardonly), '');
     } else {
-        return join(', ', core_tag_tag::get_item_tags_array('', $record_type, $record_id, $official, 0, false));
+        return join(', ', core_tag_tag::get_item_tags_array('', $record_type, $record_id, $standardonly, 0, false));
     }
 }
 
@@ -2856,7 +2859,8 @@ function tag_add($tags, $type="default") {
     if (!is_array($tags)) {
         $tags = array($tags);
     }
-    $objects = core_tag_tag::create_if_missing(core_tag_collection::get_default(), $tags, $type === 'official');
+    $objects = core_tag_tag::create_if_missing(core_tag_collection::get_default(), $tags,
+            $type === 'official');
 
     // New function returns the tags in different format, for BC we keep the format that this function used to have.
     $rv = array();
@@ -3007,7 +3011,7 @@ function tag_print_cloud($tagset=null, $nr_of_tags=150, $return=false, $sort='')
     if (is_null($tagset)) {
         // No tag set received, so fetch tags from database.
         // Always add query by tagcollid even when it's not known to make use of the table index.
-        $tagcloud = core_tag_collection::get_tag_cloud(0, '', $nr_of_tags, $sort);
+        $tagcloud = core_tag_collection::get_tag_cloud(0, false, $nr_of_tags, $sort);
     } else {
         $tagsincloud = $tagset;
 
index b75f173..ff38252 100644 (file)
@@ -163,6 +163,8 @@ class MoodleExcelWorksheet {
         $name = strtr(trim($name, "'"), '[]*/\?:', '       ');
         // Shorten the title if necessary.
         $name = core_text::substr($name, 0, 31);
+        // After the substr, we might now have a single quote on the end.
+        $name = trim($name, "'");
 
         if ($name === '') {
             // Name is required!
index a0526c5..6114f99 100644 (file)
@@ -260,4 +260,90 @@ class core_external extends external_api {
                 'string' => new external_value(PARAM_RAW, 'translated string'))
             ));
     }
+
+    /**
+     * Returns description of get_fragment parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.1
+     */
+    public static function get_fragment_parameters() {
+        return new external_function_parameters(
+            array(
+                'component' => new external_value(PARAM_COMPONENT, 'Component for the callback e.g. mod_assign'),
+                'callback' => new external_value(PARAM_ALPHANUMEXT, 'Name of the callback to execute'),
+                'contextid' => new external_value(PARAM_INT, 'Context ID that the fragment is from'),
+                'args' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'name' => new external_value(PARAM_ALPHANUMEXT, 'param name'),
+                            'value' => new external_value(PARAM_RAW, 'param value')
+                        )
+                    ), 'args for the callback are optional', VALUE_OPTIONAL
+                )
+            )
+        );
+    }
+
+    /**
+     * Get a HTML fragment for inserting into something. Initial use is for inserting mforms into
+     * a page using AJAX.
+     * This web service is designed to be called only via AJAX and not directly.
+     * Callbacks that are called by this web service are responsible for doing the appropriate security checks
+     * to access the information returned. This only does minimal validation on the context.
+     *
+     * @param string $component Name of the component.
+     * @param string $callback Function callback name.
+     * @param int $contextid Context ID this fragment is in.
+     * @param array $args optional arguments for the callback.
+     * @return array HTML and JavaScript fragments for insertion into stuff.
+     * @since Moodle 3.1
+     */
+    public static function get_fragment($component, $callback, $contextid, $args = null) {
+        global $OUTPUT, $PAGE;
+
+        $params = self::validate_parameters(self::get_fragment_parameters(),
+                array(
+                    'component' => $component,
+                    'callback' => $callback,
+                    'contextid' => $contextid,
+                    'args' => $args
+                )
+        );
+
+        // Reformat arguments into something less unwieldy.
+        $arguments = array();
+        foreach ($params['args'] as $paramargument) {
+            $arguments[$paramargument['name']] = $paramargument['value'];
+        }
+
+        $context = context::instance_by_id($contextid);
+        self::validate_context($context);
+
+        // Hack alert: Forcing bootstrap_renderer to initiate moodle page.
+        $OUTPUT->header();
+
+        // Overwriting page_requirements_manager with the fragment one so only JS included from
+        // this point is returned to the user.
+        $PAGE->start_collecting_javascript_requirements();
+        $data = component_callback($params['component'], 'output_fragment_' . $params['callback'], $arguments);
+        $jsfooter = $PAGE->requires->get_end_code();
+        $output = array('html' => $data, 'javascript' => $jsfooter);
+        return $output;
+    }
+
+    /**
+     * Returns description of get_fragment() result value
+     *
+     * @return array
+     * @since Moodle 3.1
+     */
+    public static function get_fragment_returns() {
+        return new external_single_structure(
+            array(
+                'html' => new external_value(PARAM_RAW, 'HTML fragment.'),
+                'javascript' => new external_value(PARAM_RAW, 'JavaScript fragment')
+            )
+        );
+    }
 }
index 4da13f2..4b1e7e8 100644 (file)
@@ -4506,6 +4506,14 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file_not_found();
             }
 
+            if ($context->get_course_context(false)) {
+                // If block is in course context, then check if user has capability to access course.
+                require_course_login($course);
+            } else if ($CFG->forcelogin) {
+                // If user is logged out, bp record will not be visible, even if the user would have access if logged in.
+                require_login();
+            }
+
             $bprecord = $DB->get_record('block_positions', array('contextid' => $context->id, 'blockinstanceid' => $context->instanceid));
             // User can't access file, if block is hidden or doesn't have block:view capability
             if (($bprecord && !$bprecord->visible) || !has_capability('moodle/block:view', $context)) {
index dd8ccce..73394f4 100644 (file)
@@ -91,15 +91,15 @@ class MoodleQuickForm_modgrade extends MoodleQuickForm_group{
         $langscale = get_string('modgradetypescale', 'grades');
         $scaleselect = @MoodleQuickForm::createElement('select', 'modgrade_scale', $langscale, $scales, $attributes);
         $scaleselect->setHiddenLabel = false;
-        $scaleselect->_generateId();
-        $scaleselectid = $scaleselect->getAttribute('id');
+        $scaleselectid = $this->generate_modgrade_subelement_id('modgrade_scale');
+        $scaleselect->updateAttributes(array('id' => $scaleselectid));
 
         // Maximum grade textbox.
         $langmaxgrade = get_string('modgrademaxgrade', 'grades');
         $maxgrade = @MoodleQuickForm::createElement('text', 'modgrade_point', $langmaxgrade, array());
         $maxgrade->setHiddenLabel = false;
-        $maxgrade->_generateId();
-        $maxgradeid = $maxgrade->getAttribute('id');
+        $maxgradeid = $this->generate_modgrade_subelement_id('modgrade_point');
+        $maxgrade->updateAttributes(array('id' => $maxgradeid));
 
         // Grade type select box.
         $gradetype = array(
@@ -110,7 +110,8 @@ class MoodleQuickForm_modgrade extends MoodleQuickForm_group{
         $langtype = get_string('modgradetype', 'grades');
         $typeselect = @MoodleQuickForm::createElement('select', 'modgrade_type', $langtype, $gradetype, $attributes, true);
         $typeselect->setHiddenLabel = false;
-        $typeselect->_generateId();
+        $typeselectid = $this->generate_modgrade_subelement_id('modgrade_type');
+        $typeselect->updateAttributes(array('id' => $typeselectid));
 
         // Add elements.
 
@@ -315,4 +316,17 @@ class MoodleQuickForm_modgrade extends MoodleQuickForm_group{
         return parent::onQuickFormEvent($event, $arg, $caller);
     }
 
+    /**
+     * Generates the id attribute for the subelement of the modgrade group.
+     *
+     * Uses algorithm similar to what {@link HTML_QuickForm_element::_generateId()}
+     * does but takes the name of the wrapping modgrade group into account.
+     *
+     * @param string $subname the name of the HTML_QuickForm_element in this modgrade group
+     * @return string
+     */
+    protected function generate_modgrade_subelement_id($subname) {
+        $gid = str_replace(array('[', ']'), array('_', ''), $this->getName());
+        return clean_param('id_'.$gid.'_'.$subname, PARAM_ALPHANUMEXT);
+    }
 }
index 096bbd9..3a66e69 100644 (file)
@@ -18,7 +18,7 @@
 /**
  * Tag autocomplete field.
  *
- * Contains HTML class for editing tags, both official and personal.
+ * Contains HTML class for editing tags, both standard and not.
  *
  * @package   core_form
  * @copyright 2009 Tim Hunt
@@ -31,7 +31,7 @@ require_once($CFG->libdir . '/form/autocomplete.php');
 /**
  * Form field type for editing tags.
  *
- * HTML class for editing tags, both official and personal.
+ * HTML class for editing tags, both standard and not.
  *
  * @package   core_form
  * @copyright 2009 Tim Hunt
@@ -41,12 +41,14 @@ class MoodleQuickForm_tags extends MoodleQuickForm_autocomplete {
     /**
      * Inidcates that the user should be the usual interface, with the official
      * tags listed seprately, and a text box where they can type anything.
+     * @deprecated since 3.1
      * @var int
      */
     const DEFAULTUI = 'defaultui';
 
     /**
      * Indicates that the user should only be allowed to select official tags.
+     * @deprecated since 3.1
      * @var int
      */
     const ONLYOFFICIAL = 'onlyofficial';
@@ -54,14 +56,15 @@ class MoodleQuickForm_tags extends MoodleQuickForm_autocomplete {
     /**
      * Indicates that the user should just be given a text box to type in (they
      * can still type official tags though.
+     * @deprecated since 3.1
      * @var int
      */
     const NOOFFICIAL = 'noofficial';
 
     /**
-     * @var boolean $showingofficial Official tags shown? (if not, then don't show link to manage official tags).
+     * @var boolean $showstandard Standard tags suggested? (if not, then don't show link to manage standard tags).
      */
-    protected $showingofficial = false;
+    protected $showstandard = false;
 
     /**
      * Options passed when creating an element.
@@ -83,25 +86,33 @@ class MoodleQuickForm_tags extends MoodleQuickForm_autocomplete {
         if (!empty($options)) {
             // Only execute it when the element was created and $options has values set by user.
             // In onQuickFormEvent() we make sure that $options is not empty even if developer left it empty.
-            if (empty($options['display'])) {
-                $options['display'] = self::DEFAULTUI;
+            $showstandard = core_tag_tag::BOTH_STANDARD_AND_NOT;
+            if (isset($options['showstandard'])) {
+                $showstandard = $options['showstandard'];
+            } else if (isset($options['display'])) {
+                debugging('Option "display" is deprecated, each tag area can be configured to show standard tags or not ' .
+                    'by admin or manager. If it is necessary for the developer to override it, please use "showstandard" option',
+                    DEBUG_DEVELOPER);
+                if ($options['display'] === self::NOOFFICIAL) {
+                    $showstandard = core_tag_tag::HIDE_STANDARD;
+                } else if ($options['display'] === self::ONLYOFFICIAL) {
+                    $showstandard = core_tag_tag::STANDARD_ONLY;
+                }
+            } else if (!empty($options['component']) && !empty($options['itemtype'])) {
+                $showstandard = core_tag_area::get_showstandard($options['component'], $options['itemtype']);
             }
-            $this->tagsoptions = $options;
 
-            $this->showingofficial = $options['display'] != self::NOOFFICIAL;
+            $this->tagsoptions = $options;
 
-            if ($this->showingofficial) {
-                $validoptions = $this->load_official_tags();
+            $this->showstandard = ($showstandard != core_tag_tag::HIDE_STANDARD);
+            if ($this->showstandard) {
+                $validoptions = $this->load_standard_tags();
             }
             // Option 'tags' allows us to type new tags.
-            if ($options['display'] == self::ONLYOFFICIAL) {
-                $attributes['tags'] = false;
-            } else {
-                $attributes['tags'] = true;
-            }
+            $attributes['tags'] = ($showstandard != core_tag_tag::STANDARD_ONLY);
             $attributes['multiple'] = 'multiple';
             $attributes['placeholder'] = get_string('entertags', 'tag');
-            $attributes['showsuggestions'] = $this->showingofficial;
+            $attributes['showsuggestions'] = $this->showstandard;
         }
 
         parent::__construct($elementName, $elementLabel, $validoptions, $attributes);
@@ -149,7 +160,7 @@ class MoodleQuickForm_tags extends MoodleQuickForm_autocomplete {
     }
 
     /**
-     * Finds the tag collection to use for official tag selector
+     * Finds the tag collection to use for standard tag selector
      *
      * @return int
      */
@@ -181,9 +192,9 @@ class MoodleQuickForm_tags extends MoodleQuickForm_autocomplete {
         global $OUTPUT;
 
         $managelink = '';
-        if (has_capability('moodle/tag:manage', context_system::instance()) && $this->showingofficial) {
+        if (has_capability('moodle/tag:manage', context_system::instance()) && $this->showstandard) {
             $url = new moodle_url('/tag/manage.php', array('tc' => $this->get_tag_collection()));
-            $managelink = ' ' . $OUTPUT->action_link($url, get_string('manageofficialtags', 'tag'));
+            $managelink = ' ' . $OUTPUT->action_link($url, get_string('managestandardtags', 'tag'));
         }
 
         return parent::toHTML() . $managelink;
@@ -205,16 +216,16 @@ class MoodleQuickForm_tags extends MoodleQuickForm_autocomplete {
     }
 
     /**
-     * Internal function to load official tags
+     * Internal function to load standard tags
      */
-    protected function load_official_tags() {
+    protected function load_standard_tags() {
         global $CFG, $DB;
         if (!$this->is_tagging_enabled()) {
             return array();
         }
         $namefield = empty($CFG->keeptagnamecase) ? 'name' : 'rawname';
         $tags = $DB->get_records_menu('tag',
-            array('tagtype' => 'official', 'tagcollid' => $this->get_tag_collection()),
+            array('isstandard' => 1, 'tagcollid' => $this->get_tag_collection()),
             $namefield, 'id,' . $namefield);
         return array_combine($tags, $tags);
     }
index a1769fb..ccda806 100644 (file)
@@ -270,6 +270,42 @@ function ldap_find_userdn($ldapconnection, $username, $contexts, $objectclass, $
     return $ldap_user_dn;
 }
 
+/**
+ * Normalise the supplied objectclass filter.
+ *
+ * This normalisation is a rudimentary attempt to format the objectclass filter correctly.
+ *
+ * @param string $objectclass The objectclass to normalise
+ * @param string $default The default objectclass value to use if no objectclass was supplied
+ * @return string The normalised objectclass.
+ */
+function ldap_normalise_objectclass($objectclass, $default = '*') {
+    if (empty($objectclass)) {
+        // Can't send empty filter.
+        $return = sprintf('(objectClass=%s)', $default);
+    } else if (stripos($objectclass, 'objectClass=') === 0) {
+        // Value is 'objectClass=some-string-here', so just add () around the value (filter _must_ have them).
+        $return = sprintf('(%s)', $objectclass);
+    } else if (stripos($objectclass, '(') !== 0) {
+        // Value is 'some-string-not-starting-with-left-parentheses', which is assumed to be the objectClass matching value.
+        // Build a valid filter using the value it.
+        $return = sprintf('(objectClass=%s)', $objectclass);
+    } else {
+        // There is an additional possible value '(some-string-here)', that can be used to specify any valid filter
+        // string, to select subsets of users based on any criteria.
+        //
+        // For example, we could select the users whose objectClass is 'user' and have the 'enabledMoodleUser'
+        // attribute, with something like:
+        //
+        // (&(objectClass=user)(enabledMoodleUser=1))
+        //
+        // In this particular case we don't need to do anything, so leave $this->config->objectclass as is.
+        $return = $objectclass;
+    }
+
+    return $return;
+}
+
 /**
  * Returns values like ldap_get_entries but is binary compatible and
  * returns all attributes as array.
diff --git a/lib/outputfragmentrequirementslib.php b/lib/outputfragmentrequirementslib.php
new file mode 100644 (file)
index 0000000..0ae13fb
--- /dev/null
@@ -0,0 +1,127 @@
+<?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/>.
+
+/**
+ * Library functions to facilitate the use of JavaScript in Moodle.
+ *
+ * @copyright 2016 Adrian Greeve <adrian@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package core
+ * @category output
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * This requirements manager captures the appropriate html for creating a fragment to
+ * be inserted elsewhere.
+ *
+ * @copyright 2016 Adrian Greeve <adrian@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 3.1
+ * @package core
+ * @category output
+ */
+class fragment_requirements_manager extends page_requirements_manager {
+
+    /**
+     * Page fragment constructor.
+     */
+    public function __construct() {
+        parent::__construct();
+        // As this is a fragment the header should already be done.
+        $this->headdone = true;
+    }
+
+    /**
+     * 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 = '';
+
+        // 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;
+    }
+
+
+    /**
+     * Generate any HTML that needs to go at the end of the page.
+     *
+     * @return string the HTML code to to at the end of the page.
+     */
+    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();
+
+        // All the other linked scripts - there should be as few as possible.
+        if ($this->jsincludes['footer']) {
+            foreach ($this->jsincludes['footer'] as $url) {
+                $output .= html_writer::script('', $url);
+            }
+        }
+
+        if (!empty($this->stringsforjs)) {
+            // Add all needed strings.
+            $strings = array();
+            foreach ($this->stringsforjs as $component => $v) {
+                foreach ($v as $indentifier => $langstring) {
+                    $strings[$component][$indentifier] = $langstring->out();
+                }
+            }
+            // Append don't overwrite.
+            $output .= html_writer::script('require(["jquery"], function($) {
+                M.str = $.extend(true, M.str, ' . json_encode($strings) . ');
+            });');
+        }
+
+        // Add variables.
+        if ($this->jsinitvariables['footer']) {
+            $js = '';
+            foreach ($this->jsinitvariables['footer'] as $data) {
+                list($var, $value) = $data;
+                $js .= js_writer::set_variable($var, $value, true);
+            }
+            $output .= html_writer::script($js);
+        }
+
+        $inyuijs = $this->get_javascript_code(false);
+        $ondomreadyjs = $this->get_javascript_code(true);
+        // See if this is still needed when we get to the ajax page.
+        $jsinit = $this->get_javascript_init_code();
+        $handlersjs = $this->get_event_handler_code();
+
+        // There is a global Y, make sure it is available in your scope.
+        $js = "(function() {{$inyuijs}{$ondomreadyjs}{$jsinit}{$handlersjs}})();";
+
+        $output .= html_writer::script($js);
+
+        return $output;
+    }
+}
index d46b77d..811234f 100644 (file)
@@ -824,6 +824,29 @@ class moodle_page {
         return $this->_navbar->has_items();
     }
 
+    /**
+     * Switches from the regular requirements manager to the fragment requirements manager to
+     * capture all necessary JavaScript to display a chunk of HTML such as an mform. This is for use
+     * by the get_fragment() web service and not for use elsewhere.
+     */
+    public function start_collecting_javascript_requirements() {
+        global $CFG;
+        require_once($CFG->libdir.'/outputfragmentrequirementslib.php');
+
+        // Check that the requirements manager has not already been switched.
+        if (get_class($this->_requires) == 'fragment_requirements_manager') {
+            throw new coding_exception('JavaScript collection has already been started.');
+        }
+        // The header needs to have been called to flush out the generic JavaScript for the page. This allows only
+        // JavaScript for the fragment to be collected. _wherethemewasinitialised is set when header() is called.
+        if (!empty($this->_wherethemewasinitialised)) {
+            // Change the current requirements manager over to the fragment manager to capture JS.
+            $this->_requires = new fragment_requirements_manager();
+        } else {
+            throw new coding_exception('$OUTPUT->header() needs to be called before collecting JavaScript requirements.');
+        }
+    }
+
     /**
      * Should the current user see this page in editing mode.
      * That is, are they allowed to edit this page, and are they currently in
index b502d4a..b3955ae 100644 (file)
@@ -52,6 +52,8 @@ class moodle_phpmailer extends PHPMailer {
         global $CFG;
         $this->Version   = 'Moodle '.$CFG->version;         // mailer version
         $this->CharSet   = 'UTF-8';
+        // MDL-52637: Disable the automatic TLS encryption added in v5.2.10 (9da56fc1328a72aa124b35b738966315c41ef5c6).
+        $this->SMTPAutoTLS = false;
 
         if (!empty($CFG->smtpauthtype)) {
             $this->AuthType = $CFG->smtpauthtype;
index 0da8931..bb75dcd 100644 (file)
@@ -101,7 +101,7 @@ class phpunit_util extends testing_util {
      * @return void
      */
     public static function reset_all_data($detectchanges = false) {
-        global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION;
+        global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION, $FULLME;
 
         // Stop any message redirection.
         self::stop_message_redirection();
@@ -164,6 +164,10 @@ class phpunit_util extends testing_util {
                 $warnings[] = 'Warning: unexpected change of $COURSE';
             }
 
+            if ($FULLME !== self::get_global_backup('FULLME')) {
+                $warnings[] = 'Warning: unexpected change of $FULLME';
+            }
+
             if (setlocale(LC_TIME, 0) !== $localename) {
                 $warnings[] = 'Warning: unexpected change of locale';
             }
@@ -184,6 +188,7 @@ class phpunit_util extends testing_util {
         $_SERVER = self::get_global_backup('_SERVER');
         $CFG = self::get_global_backup('CFG');
         $SITE = self::get_global_backup('SITE');
+        $FULLME = self::get_global_backup('FULLME');
         $_GET = array();
         $_POST = array();
         $_FILES = array();
@@ -299,13 +304,14 @@ class phpunit_util extends testing_util {
      * @return void
      */
     public static function bootstrap_init() {
-        global $CFG, $SITE, $DB;
+        global $CFG, $SITE, $DB, $FULLME;
 
         // backup the globals
         self::$globals['_SERVER'] = $_SERVER;
         self::$globals['CFG'] = clone($CFG);
         self::$globals['SITE'] = clone($SITE);
         self::$globals['DB'] = $DB;
+        self::$globals['FULLME'] = $FULLME;
 
         // refresh data in all tables, clear caches, etc.
         self::reset_all_data();
index 756b15c..cf191f5 100644 (file)
@@ -843,10 +843,6 @@ EOD;
             $record['name'] = core_text::strtolower($record['name']);
         }
 
-        if (!isset($record['tagtype'])) {
-            $record['tagtype'] = 'default';
-        }
-
         if (!isset($record['tagcollid'])) {
             $record['tagcollid'] = core_tag_collection::get_default();
         }
index 78ea809..cddbe93 100644 (file)
@@ -375,13 +375,22 @@ class behat_hooks extends behat_base {
      * @AfterStep
      */
     public function after_step(StepEvent $event) {
-        global $CFG;
+        global $CFG, $DB;
 
         // Save the page content if the step failed.
         if (!empty($CFG->behat_faildump_path) &&
                 $event->getResult() === StepEvent::FAILED) {
             $this->take_contentdump($event);
         }
+
+        // Abort any open transactions to prevent subsequent tests hanging.
+        // This does the same as abort_all_db_transactions(), but doesn't call error_log() as we don't
+        // want to see a message in the behat output.
+        if ($event->hasException()) {
+            if ($DB && $DB->is_transaction_started()) {
+                $DB->force_transaction_rollback();
+            }
+        }
     }
 
     /**
@@ -393,7 +402,17 @@ class behat_hooks extends behat_base {
      * @AfterScenario @_switch_window
      */
     public function after_scenario_switchwindow(ScenarioEvent $event) {
-        $this->getSession()->restart();
+        for ($count = 0; $count < self::EXTENDED_TIMEOUT; $count) {
+            try {
+                $this->getSession()->restart();
+                break;
+            } catch (DriverException $e) {
+                // Wait for timeout and try again.
+                sleep(self::TIMEOUT);
+            }
+        }
+        // If session is not restarted above then it will try to start session before next scenario
+        // and if that fails then exception will be thrown.
     }
 
     /**
index 18e5edc..cfe314c 100644 (file)
@@ -550,6 +550,33 @@ class core_formslib_testcase extends advanced_testcase {
         $data = $mform->get_data();
         $this->assertSame($expectedvalues, (array) $data);
     }
+
+    /**
+     * MDL-52873
+     */
+    public function test_multiple_modgrade_fields() {
+        $this->resetAfterTest(true);
+
+        $form = new formslib_multiple_modgrade_form();
+        ob_start();
+        $form->display();
+        $html = ob_get_clean();
+
+        $this->assertTag(array('id' => 'fgroup_id_grade1'), $html);
+        $this->assertTag(array('id' => 'id_grade1_modgrade_type'), $html);
+        $this->assertTag(array('id' => 'id_grade1_modgrade_point'), $html);
+        $this->assertTag(array('id' => 'id_grade1_modgrade_scale'), $html);
+
+        $this->assertTag(array('id' => 'fgroup_id_grade2'), $html);
+        $this->assertTag(array('id' => 'id_grade2_modgrade_type'), $html);
+        $this->assertTag(array('id' => 'id_grade2_modgrade_point'), $html);
+        $this->assertTag(array('id' => 'id_grade2_modgrade_scale'), $html);
+
+        $this->assertTag(array('id' => 'fgroup_id_grade_3'), $html);
+        $this->assertTag(array('id' => 'id_grade_3_modgrade_type'), $html);
+        $this->assertTag(array('id' => 'id_grade_3_modgrade_point'), $html);
+        $this->assertTag(array('id' => 'id_grade_3_modgrade_scale'), $html);
+    }
 }
 
 
@@ -822,3 +849,15 @@ class formslib_clean_value extends moodleform {
             'repeatnamedgroup[repeatnamedgroupel2]' => array('type' => PARAM_INT)), 'repeatablenamedgroup', 'add', 0);
     }
 }
+
+/**
+ * Used to test that modgrade fields get unique id attributes.
+ */
+class formslib_multiple_modgrade_form extends moodleform {
+    public function definition() {
+        $mform = $this->_form;
+        $mform->addElement('modgrade', 'grade1', 'Grade 1');
+        $mform->addElement('modgrade', 'grade2', 'Grade 2');
+        $mform->addElement('modgrade', 'grade[3]', 'Grade 3');
+    }
+}
index 0864333..909c3f7 100644 (file)
@@ -166,4 +166,45 @@ class core_ldaplib_testcase extends advanced_testcase {
             $this->assertSame($test['expected'], ldap_stripslashes($test['test']));
         }
     }
+
+    /**
+     * Tests for ldap_normalise_objectclass.
+     *
+     * @dataProvider ldap_normalise_objectclass_provider
+     * @param array $args Arguments passed to ldap_normalise_objectclass
+     * @param string $expected The expected objectclass filter
+     */
+    public function test_ldap_normalise_objectclass($args, $expected) {
+        $this->assertEquals($expected, call_user_func_array('ldap_normalise_objectclass', $args));
+    }
+
+    /**
+     * Data provider for the test_ldap_normalise_objectclass testcase.
+     *
+     * @return array of testcases.
+     */
+    public function ldap_normalise_objectclass_provider() {
+        return array(
+            'Empty value' => array(
+                array(null),
+                '(objectClass=*)',
+            ),
+            'Empty value with different default' => array(
+                array(null, 'lion'),
+                '(objectClass=lion)',
+            ),
+            'Supplied unwrapped objectClass' => array(
+                array('objectClass=tiger'),
+                '(objectClass=tiger)',
+            ),
+            'Supplied string value' => array(
+                array('leopard'),
+                '(objectClass=leopard)',
+            ),
+            'Supplied complex' => array(
+                array('(&(objectClass=cheetah)(enabledMoodleUser=1))'),
+                '(&(objectClass=cheetah)(enabledMoodleUser=1))',
+            ),
+        );
+    }
 }
index a463e9d..45966f6 100644 (file)
@@ -112,6 +112,11 @@ class login_signup_form extends moodleform {
     function definition_after_data(){
         $mform = $this->_form;
         $mform->applyFilter('username', 'trim');
+
+        // Trim required name fields.
+        foreach (useredit_get_required_name_fields() as $field) {
+            $mform->applyFilter($field, 'trim');
+        }
     }
 
     function validation($data, $files) {
index b491a6b..1a9b48b 100644 (file)
@@ -27,12 +27,13 @@ define('NO_MOODLE_COOKIES', true);
 
 require_once(dirname(dirname(__FILE__)) . '/config.php');
 
+// Allow CORS requests.
+header('Access-Control-Allow-Origin: *');
+
 $username = required_param('username', PARAM_USERNAME);
 $password = required_param('password', PARAM_RAW);
 $serviceshortname  = required_param('service',  PARAM_ALPHANUMEXT);
 
-// Allow CORS requests.
-header('Access-Control-Allow-Origin: *');
 echo $OUTPUT->header();
 
 if (!$CFG->enablewebservices) {
index 745618d..0e95f6c 100644 (file)
@@ -25,13 +25,13 @@ Feature: Check that messages can be deleted
     And "Delete" "link" should exist in the "#message_1" "css_element"
     And "Delete" "link" should exist in the "#message_2" "css_element"
     # Confirm that there is a confirmation box before deleting, and that when we cancel the messages remain.
-    And I click on "#message_2" "css_element"
+    And I hover "#message_2" "css_element"
     And I click on "Delete" "link" in the "#message_2" "css_element"
     And I press "Cancel"
     And I should see "Hey bud, what's happening?"
     And I should see "Whoops, forgot to mention that I drank all your beers. Lol."
     # Confirm we can delete a message and then can no longer see it.
-    And I click on "#message_2" "css_element"
+    And I hover "#message_2" "css_element"
     And I click on "Delete" "link" in the "#message_2" "css_element"
     And I press "Delete"
     And I should see "Hey bud, what's happening?"
@@ -69,10 +69,10 @@ Feature: Check that messages can be deleted
     And "Delete" "link" should exist in the "#message_3" "css_element"
     And "Delete" "link" should exist in the "#message_4" "css_element"
     # Now, delete one of the messages that User 1 sent and one by User 2.
-    And I click on "#message_1" "css_element"
+    And I hover "#message_1" "css_element"
     And I click on "Delete" "link" in the "#message_1" "css_element"
     And I press "Delete"
-    And I click on "#message_2" "css_element"
+    And I hover "#message_2" "css_element"
     And I click on "Delete" "link" in the "#message_2" "css_element"
     And I press "Delete"
     # Confirm that the messages are no longer listed.
@@ -111,7 +111,7 @@ Feature: Check that messages can be deleted
     # Send a message from the admin to User 1
     And I send "Hey there, this is the all-powerful administrator. Obey my commands." message to "User 1" user
     # Check the admin is still able to delete messages.
-    And I click on "#message_1" "css_element"
+    And I hover "#message_1" "css_element"
     And I click on "Delete" "link" in the "#message_1" "css_element"
     And I press "Delete"
     And I should not see "Hey there, this is the all-powerful administrator. Obey my commands."
index 1d5ec06..6e80cad 100644 (file)
@@ -774,6 +774,9 @@ class assign_grading_table extends table_sql implements renderable {
         $selectcol .= '<input type="hidden"
                               name="grademodified_' . $row->userid . '"
                               value="' . $row->timemarked . '"/>';
+        $selectcol .= '<input type="hidden"
+                              name="gradeattempt_' . $row->userid . '"
+                              value="' . $row->attemptnumber . '"/>';
         return $selectcol;
     }
 
index f353948..46008dc 100644 (file)
@@ -5399,6 +5399,7 @@ class assign {
         // Gets a list of possible users and look for values based upon that.
         foreach ($participants as $userid => $unused) {
             $modified = optional_param('grademodified_' . $userid, -1, PARAM_INT);
+            $attemptnumber = optional_param('gradeattempt_' . $userid, -1, PARAM_INT);
             // Gather the userid, updated grade and last modified value.
             $record = new stdClass();
             $record->userid = $userid;
@@ -5410,6 +5411,7 @@ class assign {
                 // This user was not in the grading table.
                 continue;
             }
+            $record->attemptnumber = $attemptnumber;
             $record->lastmodified = $modified;
             $record->gradinginfo = grade_get_grades($this->get_course()->id,
                                                     'mod',
@@ -5428,19 +5430,20 @@ class assign {
         $params['assignid2'] = $this->get_instance()->id;
 
         // Check them all for currency.
-        $grademaxattempt = 'SELECT mxg.userid, MAX(mxg.attemptnumber) AS maxattempt
-                            FROM {assign_grades} mxg
-                            WHERE mxg.assignment = :assignid1 GROUP BY mxg.userid';
-
-        $sql = 'SELECT u.id as userid, g.grade as grade, g.timemodified as lastmodified, uf.workflowstate, uf.allocatedmarker
-                    FROM {user} u
-                LEFT JOIN ( ' . $grademaxattempt . ' ) gmx ON u.id = gmx.userid
-                LEFT JOIN {assign_grades} g ON
-                    u.id = g.userid AND
-                    g.assignment = :assignid2 AND
-                    g.attemptnumber = gmx.maxattempt
-                LEFT JOIN {assign_user_flags} uf ON uf.assignment = g.assignment AND uf.userid = g.userid
-                WHERE u.id ' . $userids;
+        $grademaxattempt = 'SELECT s.userid, s.attemptnumber AS maxattempt
+                              FROM {assign_submission} s
+                             WHERE s.assignment = :assignid1 AND s.latest = 1';
+
+        $sql = 'SELECT u.id AS userid, g.grade AS grade, g.timemodified AS lastmodified,
+                       uf.workflowstate, uf.allocatedmarker, gmx.maxattempt AS attemptnumber
+                  FROM {user} u
+             LEFT JOIN ( ' . $grademaxattempt . ' ) gmx ON u.id = gmx.userid
+             LEFT JOIN {assign_grades} g ON
+                       u.id = g.userid AND
+                       g.assignment = :assignid2 AND
+                       g.attemptnumber = gmx.maxattempt
+             LEFT JOIN {assign_user_flags} uf ON uf.assignment = g.assignment AND uf.userid = g.userid
+                 WHERE u.id ' . $userids;
         $currentgrades = $DB->get_recordset_sql($sql, $params);
 
         $modifiedusers = array();
@@ -5504,7 +5507,9 @@ class assign {
                 if ($this->grading_disabled($modified->userid)) {
                     continue;
                 }
-                if ((int)$current->lastmodified > (int)$modified->lastmodified) {
+                $badmodified = (int)$current->lastmodified > (int)$modified->lastmodified;
+                $badattempt = (int)$current->attemptnumber != (int)$modified->attemptnumber;
+                if ($badmodified || $badattempt) {
                     // Error - record has been modified since viewing the page.
                     return get_string('errorrecordmodified', 'assign');
                 } else {
index 246b147..f03c9b2 100644 (file)
@@ -56,3 +56,112 @@ Feature: In an assignment, students start a new attempt based on their previous
     And I follow "View/grade all submissions"
     And I click on "Grade Student 1" "link" in the "Student 1" "table_row"
     And I should see "I'm the student first submission"
+
+  @javascript @_alert
+  Scenario: Allow new attempt does not display incorrect error message on group submission
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | student1 | Student | 1 | student1@example.com |
+      | student2 | Student | 2 | student2@example.com |
+      | student3 | Student | 3 | student3@example.com |
+      | student4 | Student | 4 | student4@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+      | student2 | C1 | student |
+      | student3 | C1 | student |
+      | student4 | C1 | student |
+    And the following "groups" exist:
+      | name | course | idnumber |
+      | Group 1 | C1 | G1 |
+      | Group 2 | C1 | G2 |
+    And the following "group members" exist:
+      | user | group |
+      | student1 | G1 |
+      | student2 | G1 |
+      | student3 | G2 |
+      | student4 | G2 |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test assignment name |
+      | Description | Test assignment description |
+      | assignsubmission_onlinetext_enabled | 1 |
+      | assignsubmission_file_enabled | 0 |
+      | Students submit in groups | Yes |
+      | Attempts reopened | Manually |
+      | Maximum attempts | 3 |
+      | Group mode | Separate groups |
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test assignment name"
+    And I press "Add submission"
+    And I set the following fields to these values:
+      | Online text | I'm the student's first submission |
+    And I press "Save changes"
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Test assignment name"
+    When I follow "View/grade all submissions"
+    Then "Student 1" row "Status" column of "generaltable" table should contain "Submitted for grading"
+    And "Student 2" row "Status" column of "generaltable" table should contain "Submitted for grading"
+    And "Student 3" row "Status" column of "generaltable" table should contain "No submission"
+    And "Student 4" row "Status" column of "generaltable" table should contain "No submission"
+    And I click on "Quick grading" "checkbox"
+    And I click on "Student 1" "checkbox"
+    And I set the field "User grade" to "60.0"
+    And I press "Save all quick grading changes"
+    And I should see "The grade changes were saved"
+    And I press "Continue"
+    And I click on "Student 1" "checkbox"
+    And I set the following fields to these values:
+      | operation | Allow another attempt |
+    And I click on "Go" "button" confirming the dialogue
+    And I should not see "The grades were not saved because someone has modified one or more records more recently than when you loaded the page."
+# Behat tests for the group submission, should be uncommented once the MDL-48216 is fixed.
+#    And I log out
+#    And I log in as "student3"
+#    And I follow "Course 1"
+#    And I follow "Test assignment name"
+#    #And I should see "This is attempt 1 ( 3 attempts allowed )."
+#    And I press "Add submission"
+#    And I set the following fields to these values:
+#      | Online text | I'm the student's 3 group 2 first attempt |
+#    And I press "Save changes"
+#    And I log out
+#    And I log in as "teacher1"
+#    And I follow "Course 1"
+#    And I follow "Test assignment name"
+#    And I follow "View/grade all submissions"
+#    And "Student 1" row "Status" column of "generaltable" table should contain "Reopened"
+#    And "Student 2" row "Status" column of "generaltable" table should contain "Reopened"
+#    And "Student 3" row "Status" column of "generaltable" table should contain "Submitted for grading"
+#    And "Student 4" row "Status" column of "generaltable" table should contain "Submitted for grading"
+#    And I click on "Grade Student 3" "link" in the "Student 3" "table_row"
+#    And I set the following fields to these values:
+#      | Allow another attempt | 1 |
+#    And I press "Save changes"
+#    And I log out
+#    And I log in as "student4"
+#    And I follow "Course 1"
+#    And I follow "Test assignment name"
+#    #And I should see "This is attempt 2 ( 3 attempts allowed )."
+#    And I press "Add submission"
+#    And I set the following fields to these values:
+#      | Online text | I'm the student's 4 group 2 second attempt |
+#    And I press "Save changes"
+#    And I log out
+#    And I log in as "teacher1"
+#    And I follow "Course 1"
+#    And I follow "Test assignment name"
+#    And I follow "View/grade all submissions"
+#    And I click on "Grade Student 4" "link" in the "Student 1" "table_row"
+    #And I should see "This is attempt 2 (3 attempts allowed)"
index e277c25..89c125a 100644 (file)
@@ -51,7 +51,6 @@ Feature: In an assignment, teachers can edit feedback for a students previous su
     And I log in as "student2"
     And I follow "Course 1"
     And I follow "Test assignment name"
-    And I click on ".mod-assign-history-link" "css_element"
     And I should see "I'm the teacher first feedback" in the "Feedback comments" "table_row"
     And I log out
     When I log in as "teacher1"
@@ -60,7 +59,6 @@ Feature: In an assignment, teachers can edit feedback for a students previous su
     And I follow "View/grade all submissions"
     And I click on "Grade Student 2" "link" in the "Student 2" "table_row"
     And I click on ".mod-assign-history-link" "css_element"
-    And I follow "Edit the grade and feedback for attempt number 1"
     And I set the following fields to these values:
       | Grade | 50 |
       | Feedback comments | I'm the teacher second feedback |
index 14d68ec..787350b 100644 (file)
@@ -436,6 +436,7 @@ class assign_events_testcase extends mod_assign_base_testcase {
 
         $data = array(
             'grademodified_' . $this->students[0]->id => time(),
+            'gradeattempt_' . $this->students[0]->id => '',
             'quickgrade_' . $this->students[0]->id => '60.0',
             'quickgrade_' . $this->students[0]->id . '_workflowstate' => 'inmarking'
         );
@@ -565,8 +566,10 @@ class assign_events_testcase extends mod_assign_base_testcase {
         // Test process_save_quick_grades.
         $sink = $this->redirectEvents();
 
+        $grade = $assign->get_user_grade($this->students[0]->id, false);
         $data = array(
             'grademodified_' . $this->students[0]->id => time(),
+            'gradeattempt_' . $this->students[0]->id => $grade->attemptnumber,
             'quickgrade_' . $this->students[0]->id => '60.0'
         );
         $assign->testable_process_save_quick_grades($data);
index dee37fa..547ac86 100644 (file)
@@ -2329,5 +2329,75 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
         $this->assertTrue(in_array($this->extrastudents[0]->id, $allgroupmembers));
         $this->assertTrue(in_array($this->extrastudents[1]->id , $allgroupmembers));
     }
-}
 
+    /**
+     * Test the quicksave grades processor
+     */
+    public function test_process_save_quick_grades() {
+        $this->editingteachers[0]->ignoresesskey = true;
+        $this->setUser($this->editingteachers[0]);
+
+        $assign = $this->create_instance(array('attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL));
+
+        // Initially grade the user.
+        $grade = $assign->get_user_grade($this->students[0]->id, false);
+        if (!$grade) {
+            $grade = new stdClass();
+            $grade->attemptnumber = '';
+            $grade->timemodified = '';
+        }
+        $data = array(
+            'grademodified_' . $this->students[0]->id => $grade->timemodified,
+            'gradeattempt_' . $this->students[0]->id => $grade->attemptnumber,
+            'quickgrade_' . $this->students[0]->id => '60.0'
+        );
+        $result = $assign->testable_process_save_quick_grades($data);
+        $this->assertContains(get_string('quickgradingchangessaved', 'assign'), $result);
+        $grade = $assign->get_user_grade($this->students[0]->id, false);
+        $this->assertEquals('60.0', $grade->grade);
+
+        // Attempt to grade with a past attempts grade info.
+        $assign->testable_process_add_attempt($this->students[0]->id);
+        $data = array(
+            'grademodified_' . $this->students[0]->id => $grade->timemodified,
+            'gradeattempt_' . $this->students[0]->id => $grade->attemptnumber,
+            'quickgrade_' . $this->students[0]->id => '50.0'
+        );
+        $result = $assign->testable_process_save_quick_grades($data);
+        $this->assertContains(get_string('errorrecordmodified', 'assign'), $result);
+        $grade = $assign->get_user_grade($this->students[0]->id, false);
+        $this->assertFalse($grade);
+
+        // Attempt to grade a the attempt.
+        $submission = $assign->get_user_submission($this->students[0]->id, false);
+        $data = array(
+            'grademodified_' . $this->students[0]->id => '',
+            'gradeattempt_' . $this->students[0]->id => $submission->attemptnumber,
+            'quickgrade_' . $this->students[0]->id => '40.0'
+        );
+        $result = $assign->testable_process_save_quick_grades($data);
+        $this->assertContains(get_string('quickgradingchangessaved', 'assign'), $result);
+        $grade = $assign->get_user_grade($this->students[0]->id, false);
+        $this->assertEquals('40.0', $grade->grade);
+
+        // Catch grade update conflicts.
+        // Save old data for later.
+        $pastdata = $data;
+        // Update the grade the 'good' way.
+        $data = array(
+            'grademodified_' . $this->students[0]->id => $grade->timemodified,
+            'gradeattempt_' . $this->students[0]->id => $grade->attemptnumber,
+            'quickgrade_' . $this->students[0]->id => '30.0'
+        );
+        $result = $assign->testable_process_save_quick_grades($data);
+        $this->assertContains(get_string('quickgradingchangessaved', 'assign'), $result);
+        $grade = $assign->get_user_grade($this->students[0]->id, false);
+        $this->assertEquals('30.0', $grade->grade);
+
+        // Now update using 'old' data. Should fail.
+        $result = $assign->testable_process_save_quick_grades($pastdata);
+        $this->assertContains(get_string('errorrecordmodified', 'assign'), $result);
+        $grade = $assign->get_user_grade($this->students[0]->id, false);
+        $this->assertEquals('30.0', $grade->grade);
+    }
+}
index 73c9357..c3f098f 100644 (file)
Binary files a/mod/assign/yui/build/moodle-mod_assign-history/moodle-mod_assign-history-debug.js and b/mod/assign/yui/build/moodle-mod_assign-history/moodle-mod_assign-history-debug.js differ
index 81dd1a9..9004c3b 100644 (file)
Binary files a/mod/assign/yui/build/moodle-mod_assign-history/moodle-mod_assign-history-min.js and b/mod/assign/yui/build/moodle-mod_assign-history/moodle-mod_assign-history-min.js differ
index 73c9357..c3f098f 100644 (file)
Binary files a/mod/assign/yui/build/moodle-mod_assign-history/moodle-mod_assign-history.js and b/mod/assign/yui/build/moodle-mod_assign-history/moodle-mod_assign-history.js differ
index 9dbafaf..a30c3a0 100644 (file)
@@ -48,9 +48,17 @@ var CSS = {
                 panel.set('aria-live', 'polite');
 
                 wrapper.addClass(CSS.LINK);
-                wrapper.addClass(CSS.CLOSED);
+                if (COUNT == 1) {
+                    wrapper.addClass(CSS.OPEN);
+                } else {
+                    wrapper.addClass(CSS.CLOSED);
+                }
                 panel.addClass(CSS.PANEL);
-                panel.hide();
+                if (COUNT == 1) {
+                    panel.show();
+                } else {
+                    panel.hide();
+                }
                 link = null;
             } else {
                 link = this;
diff --git a/mod/choice/classes/event/answer_deleted.php b/mod/choice/classes/event/answer_deleted.php
new file mode 100644 (file)
index 0000000..7b8cb18
--- /dev/null
@@ -0,0 +1,114 @@
+<?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/>.
+
+/**
+ * The mod_choice answer deleted event.
+ *
+ * @package    mod_choice
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_choice\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_choice answer updated event class.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      - int choiceid: id of choice.
+ *      - int optionid: id of the option.
+ * }
+ *
+ * @package    mod_choice
+ * @since      Moodle 3.1
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class answer_deleted extends \core\event\base {
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has deleted the option with id '" . $this->other['optionid'] . "' for the
+            user with id '$this->relateduserid' from the choice activity with course module id '$this->contextinstanceid'.";
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventanswerdeleted', 'mod_choice');
+    }
+
+    /**
+     * Get URL related to the action
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/mod/choice/view.php', array('id' => $this->contextinstanceid));
+    }
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'choice_answers';
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['choiceid'])) {
+            throw new \coding_exception('The \'choiceid\' value must be set in other.');
+        }
+
+        if (!isset($this->other['optionid'])) {
+            throw new \coding_exception('The \'optionid\' value must be set in other.');
+        }
+    }
+
+    public static function get_objectid_mapping() {
+        return array('db' => 'choice_answers', 'restore' => \core\event\base::NOT_MAPPED);
+    }
+
+    public static function get_other_mapping() {
+        $othermapped = array();
+        $othermapped['choiceid'] = array('db' => 'choice', 'restore' => 'choice');
+        $othermapped['optionid'] = array('db' => 'choice_options', 'restore' => 'choice_option');
+
+        return $othermapped;
+    }
+}
diff --git a/mod/choice/classes/event/report_downloaded.php b/mod/choice/classes/event/report_downloaded.php
new file mode 100644 (file)
index 0000000..96f64c0
--- /dev/null
@@ -0,0 +1,114 @@
+<?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/>.
+
+/**
+ * The mod_choice report viewed event.
+ *
+ * @package mod_choice
+ * @copyright 2016 Stephen Bourget
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_choice\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The mod_choice report viewed event class.
+ *
+ * @property-read array $other {
+ *      Extra information about the event.
+ *
+ *      - string content: The content we are viewing.
+ *      - string format: The report format
+ *      - int choiced: The id of the choice
+ * }
+ *
+ * @package    mod_choice
+ * @since      Moodle 3.1
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class report_downloaded extends \core\event\base {
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['crud'] = 'r';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventreportdownloaded', 'mod_choice');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has downloaded the report in the '".$this->other['format']."' format for
+            the choice activity with course module id '$this->contextinstanceid'";
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/mod/choice/report.php', array('id' => $this->contextinstanceid));
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        // Report format downloaded.
+        if (!isset($this->other['content'])) {
+            throw new \coding_exception('The \'content\' value must be set in other.');
+        }
+        // Report format downloaded.
+        if (!isset($this->other['format'])) {
+            throw new \coding_exception('The \'format\' value must be set in other.');
+        }
+        // ID of the choice activity.
+        if (!isset($this->other['choiceid'])) {
+            throw new \coding_exception('The \'choiceid\' value must be set in other.');
+        }
+    }
+
+    public static function get_objectid_mapping() {
+        return false;
+    }
+
+    public static function get_other_mapping() {
+        $othermapped = array();
+        $othermapped['choiceid'] = array('db' => 'choice', 'restore' => 'choice');
+
+        return $othermapped;
+    }
+}
index 4f5bbc6..53a5bf4 100644 (file)
@@ -33,7 +33,9 @@ $string['displayhorizontal'] = 'Display horizontally';
 $string['displaymode'] = 'Display mode for the options';
 $string['displayvertical'] = 'Display vertically';
 $string['eventanswercreated'] = 'Choice made';
+$string['eventanswerdeleted'] = 'Choice answer deleted';
 $string['eventanswerupdated'] = 'Choice updated';
+$string['eventreportdownloaded'] = 'Choice report downloaded';
 $string['eventreportviewed'] = 'Choice report viewed';
 $string['expired'] = 'Sorry, this activity closed on {$a} and is no longer available';
 $string['atleastoneoption'] = 'You need to provide at least one possible answer.';
index 900cead..0deb0a8 100644 (file)
@@ -513,7 +513,7 @@ function prepare_choice_show_results($choice, $course, $cm, $allresponses) {
  * @return bool
  */
 function choice_delete_responses($attemptids, $choice, $cm, $course) {
-    global $DB, $CFG;
+    global $DB, $CFG, $USER;
     require_once($CFG->libdir.'/completionlib.php');
 
     if(!is_array($attemptids) || empty($attemptids)) {
@@ -526,16 +526,36 @@ function choice_delete_responses($attemptids, $choice, $cm, $course) {
         }
     }
 
+    $context = context_module::instance($cm->id);
     $completion = new completion_info($course);
     foreach($attemptids as $attemptid) {
         if ($todelete = $DB->get_record('choice_answers', array('choiceid' => $choice->id, 'id' => $attemptid))) {
+            // Trigger the event answer deleted.
+            $eventdata = array();
+            $eventdata['objectid'] = $todelete->id;
+            $eventdata['context'] = $context;
+            $eventdata['userid'] = $USER->id;
+            $eventdata['courseid'] = $course->id;
+            $eventdata['relateduserid'] = $todelete->userid;
+            $eventdata['other'] = array();
+            $eventdata['other']['choiceid'] = $choice->id;
+            $eventdata['other']['optionid'] = $todelete->optionid;
+            $event = \mod_choice\event\answer_deleted::create($eventdata);
+            $event->add_record_snapshot('course', $course);
+            $event->add_record_snapshot('course_modules', $cm);
+            $event->add_record_snapshot('choice', $choice);
+            $event->add_record_snapshot('choice_answers', $todelete);
+            $event->trigger();
+
             $DB->delete_records('choice_answers', array('choiceid' => $choice->id, 'id' => $attemptid));
-            // Update completion state
-            if ($completion->is_enabled($cm) && $choice->completionsubmit) {
-                $completion->update_state($cm, COMPLETION_INCOMPLETE, $attemptid);
-            }
         }
     }
+
+    // Update completion state.
+    if ($completion->is_enabled($cm) && $choice->completionsubmit) {
+        $completion->update_state($cm, COMPLETION_INCOMPLETE);
+    }
+
     return true;
 }
 
index 9620cf1..59ac1b6 100644 (file)
         }
     } else {
         $groupmode = groups_get_activity_groupmode($cm);
+
+        // Trigger the report downloaded event.
+        $eventdata = array();
+        $eventdata['context'] = $context;
+        $eventdata['courseid'] = $course->id;
+        $eventdata['other']['content'] = 'choicereportcontentviewed';
+        $eventdata['other']['format'] = $download;
+        $eventdata['other']['choiceid'] = $choice->id;
+        $event = \mod_choice\event\report_downloaded::create($eventdata);
+        $event->trigger();
+
     }
 
     // Check if we want to include responses from inactive users.
index 71bb599..d383ed4 100644 (file)
@@ -201,6 +201,42 @@ class mod_choice_events_testcase extends advanced_testcase {
         $this->assertEventContextNotUsed($event);
     }
 
+    /**
+     * Test to ensure that event data is being stored correctly.
+     */
+    public function test_answer_deleted() {
+        global $DB, $USER;
+        // Generate user data.
+        $user = $this->getDataGenerator()->create_user();
+
+        $optionids = array_keys($DB->get_records('choice_options', array('choiceid' => $this->choice->id)));
+
+        // Create the first answer.
+        choice_user_submit_response($optionids[2], $this->choice, $user->id, $this->course, $this->cm);
+        // Get the users response.
+        $answer = $DB->get_record('choice_answers', array('userid' => $user->id, 'choiceid' => $this->choice->id),
+                '*', $strictness = IGNORE_MULTIPLE);
+
+        // Redirect event.
+        $sink = $this->redirectEvents();
+        // Now delete the answer.
+        choice_delete_responses(array($answer->id), $this->choice, $this->cm, $this->course);
+
+        // Get our event event.
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        // Data checking.
+        $this->assertInstanceOf('\mod_choice\event\answer_deleted', $event);
+        $this->assertEquals($USER->id, $event->userid);
+        $this->assertEquals($user->id, $event->relateduserid);
+        $this->assertEquals(context_module::instance($this->choice->cmid), $event->get_context());
+        $this->assertEquals($this->choice->id, $event->other['choiceid']);
+        $this->assertEquals($answer->optionid, $event->other['optionid']);
+        $this->assertEventContextNotUsed($event);
+        $sink->close();
+    }
+
     /**
      * Test to ensure that event data is being stored correctly.
      */
@@ -238,6 +274,43 @@ class mod_choice_events_testcase extends advanced_testcase {
         $sink->close();
     }
 
+    /**
+     * Test to ensure that event data is being stored correctly.
+     */
+    public function test_report_downloaded() {
+        global $USER;
+
+        $this->resetAfterTest();
+
+        // Generate user data.
+        $this->setAdminUser();
+
+        $eventdata = array();
+        $eventdata['context'] = $this->context;
+        $eventdata['courseid'] = $this->course->id;
+        $eventdata['other']['content'] = 'choicereportcontentviewed';
+        $eventdata['other']['format'] = 'csv';
+        $eventdata['other']['choiceid'] = $this->choice->id;
+
+        // This is fired in a page view so we can't run this through a function.
+        $event = \mod_choice\event\report_downloaded::create($eventdata);
+
+        // Redirect event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $event = $sink->get_events();
+
+        // Data checking.
+        $this->assertCount(1, $event);
+        $this->assertInstanceOf('\mod_choice\event\report_downloaded', $event[0]);
+        $this->assertEquals($USER->id, $event[0]->userid);
+        $this->assertEquals(context_module::instance($this->choice->cmid), $event[0]->get_context());
+        $this->assertEquals('csv', $event[0]->other['format']);
+        $this->assertEquals($this->choice->id, $event[0]->other['choiceid']);
+        $this->assertEventContextNotUsed($event[0]);
+        $sink->close();
+    }
+
     /**
      * Test to ensure that event data is being stored correctly.
      */
index 0b2d8c6..2b62ca9 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015111600;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2016020100;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015111000;    // Requires this Moodle version
 $plugin->component = 'mod_choice';     // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index 98d4840..7ced17e 100644 (file)
@@ -40,13 +40,10 @@ if ($action == 'delchoice' and confirm_sesskey() and is_enrolled($context, NULL,
         and $choiceavailable) {
     $answercount = $DB->count_records('choice_answers', array('choiceid' => $choice->id, 'userid' => $USER->id));
     if ($answercount > 0) {
-        $DB->delete_records('choice_answers', array('choiceid' => $choice->id, 'userid' => $USER->id));
-
-        // Update completion state
-        $completion = new completion_info($course);
-        if ($completion->is_enabled($cm) && $choice->completionsubmit) {
-            $completion->update_state($cm, COMPLETION_INCOMPLETE);
-        }
+        $choiceanswers = $DB->get_records('choice_answers', array('choiceid' => $choice->id, 'userid' => $USER->id),
+            '', 'id');
+        $todelete = array_keys($choiceanswers);
+        choice_delete_responses($todelete, $choice, $cm, $course);
         redirect("view.php?id=$cm->id");
     }
 }
index 1c63071..9fc25e1 100644 (file)
@@ -15,6 +15,7 @@
         <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="display" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Display type of folder contents - on a separate page or inline"/>
         <FIELD NAME="showexpanded" TYPE="int" LENGTH="1" NOTNULL="true" UNSIGNED="false" DEFAULT="1" SEQUENCE="false" COMMENT="1 = expanded, 0 = collapsed for sub-folders"/>
+        <FIELD NAME="showdownloadfolder" TYPE="int" LENGTH="1" NOTNULL="true" UNSIGNED="false" DEFAULT="1" SEQUENCE="false" COMMENT="1 = show download folder button"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
index 05d297a..85ef365 100644 (file)
@@ -128,5 +128,17 @@ function xmldb_folder_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+
+    // Add showdownloadfolder option.
+    if ($oldversion < 2016020201) {
+        $table = new xmldb_table('folder');
+        $field = new xmldb_field('showdownloadfolder', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '1', 'showexpanded');
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field, 'showdownloadfolder');
+        }
+
+        upgrade_mod_savepoint(true, 2016020201, 'folder');
+    }
+
     return true;
 }
diff --git a/mod/folder/download_folder.php b/mod/folder/download_folder.php
new file mode 100644 (file)
index 0000000..178e795
--- /dev/null
@@ -0,0 +1,62 @@
+<?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/>.
+
+/**
+ * Folder download
+ *
+ * @package   mod_folder
+ * @copyright 2015 Andrew Hancox <andrewdchancox@googlemail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . "/../../config.php");
+
+$id = required_param('id', PARAM_INT);  // Course module ID.
+$cm = get_coursemodule_from_id('folder', $id, 0, true, MUST_EXIST);
+
+$course = $DB->get_record('course', array('id' => $cm->course), '*', MUST_EXIST);
+
+require_course_login($course, true, $cm);
+$context = context_module::instance($cm->id);
+require_capability('mod/folder:view', $context);
+
+$folder = $DB->get_record('folder', array('id' => $cm->instance), '*', MUST_EXIST);
+
+$foldertree = new folder_tree($folder, $cm);
+$downloadable = folder_archive_available($folder, $foldertree);
+if (!$downloadable) {
+    print_error('cannotdownloaddir', 'repository');
+}
+
+// Completion.
+$completion = new completion_info($course);
+$completion->set_module_viewed($cm);
+
+$fs = get_file_storage();
+$file = $fs->get_file($context->id, 'mod_folder', 'content', 0, '/', '.');
+if (!$file) {
+    print_error('cannotdownloaddir', 'repository');
+}
+
+$zipper   = get_file_packer('application/zip');
+$filename = clean_filename($folder->name . "-" . date("Ymd")) . ".zip";
+$temppath = make_request_directory() . $filename;
+
+if ($zipper->archive_to_pathname(array('/' => $file), $temppath)) {
+    send_temp_file($temppath, $filename);
+} else {
+    print_error('cannotdownloaddir', 'repository');
+}
index f17e18a..e37f9a1 100644 (file)
@@ -25,6 +25,7 @@
 
 $string['contentheader'] = 'Content';
 $string['dnduploadmakefolder'] = 'Unzip files and create folder';
+$string['downloadfolder'] = 'Download folder';
 $string['eventfolderupdated'] = 'Folder updated';
 $string['folder:addinstance'] = 'Add a new folder';
 $string['folder:managefiles'] = 'Manage files in folder module';
@@ -50,5 +51,12 @@ Also note that participants view actions can not be logged in this case.';
 $string['displaypage'] = 'On a separate page';
 $string['displayinline'] = 'Inline on a course page';
 $string['noautocompletioninline'] = 'Automatic completion on viewing of activity can not be selected together with "Display inline" option';
+$string['showdownloadfolder'] = 'Show download folder button';
+$string['showdownloadfolder_help'] = 'If set to \'yes\', a button will be shown to allow users to download a zip archive containing all files.';
 $string['showexpanded'] = 'Show subfolders expanded';
 $string['showexpanded_help'] = 'If set to \'yes\', subfolders are shown expanded by default; otherwise they are shown collapsed.';
+$string['maxsizetodownload'] = 'Maximum folder download size (MB)';
+$string['maxsizetodownload_help'] = 'If set then users will not be able to downlod zip archives of folder where the total size is larger than this value.';
+
+
+
index aa96de4..fe7dcd7 100644 (file)
@@ -482,3 +482,48 @@ function folder_view($folder, $course, $cm, $context) {
     $completion = new completion_info($course);
     $completion->set_module_viewed($cm);
 }
+
+/**
+ * Check if the folder can be zipped and downloaded.
+ * @param stdClass $folder
+ * @param folder_tree $foldertree
+ * @return bool True if the folder can be zipped and downloaded.
+ * @throws \dml_exception
+ */
+function folder_archive_available($folder, $foldertree) {
+    if (!$folder->showdownloadfolder) {
+        return false;
+    }
+
+    $size = folder_get_directory_size($foldertree->dir);
+    $maxsize = get_config('folder', 'maxsizetodownload') * 1024 * 1024;
+
+    if ($size == 0) {
+        return false;
+    }
+
+    if (!empty($maxsize) && $size > $maxsize) {
+        return false;
+    }
+
+    return true;
+}
+
+/**
+ * Recursively measure the size of the files in a directory.
+ * @param array $directory
+ * @return int size of directory contents in bytes
+ */
+function folder_get_directory_size($directory) {
+    $size = 0;
+
+    foreach ($directory['files'] as $file) {
+        $size += $file->get_filesize();
+    }
+
+    foreach ($directory['subdirs'] as $subdirectory) {
+        $size += folder_get_directory_size($subdirectory);
+    }
+
+    return $size;
+}
\ No newline at end of file
index 7c6764c..ec57d86 100644 (file)
@@ -63,6 +63,11 @@ class mod_folder_mod_form extends moodleform_mod {
         $mform->addElement('advcheckbox', 'showexpanded', get_string('showexpanded', 'folder'));
         $mform->addHelpButton('showexpanded', 'showexpanded', 'mod_folder');
         $mform->setDefault('showexpanded', $config->showexpanded);
+
+        // Adding option to enable downloading archive of folder.
+        $mform->addElement('advcheckbox', 'showdownloadfolder', get_string('showdownloadfolder', 'folder'));
+        $mform->addHelpButton('showdownloadfolder', 'showdownloadfolder', 'mod_folder');
+        $mform->setDefault('showdownloadfolder', true);
         //-------------------------------------------------------
         $this->standard_coursemodule_elements();
 
index cf31a3d..1731ed5 100644 (file)
@@ -65,11 +65,26 @@ class mod_folder_renderer extends plugin_renderer_base {
                 'generalbox foldertree');
 
         // Do not append the edit button on the course page.
-        if ($folder->display != FOLDER_DISPLAY_INLINE && has_capability('mod/folder:managefiles', $context)) {
+        if ($folder->display != FOLDER_DISPLAY_INLINE) {
+            $containercontents = '';
+            $downloadable = folder_archive_available($folder, $foldertree);
+
+            if ($downloadable) {
+                $containercontents .= $this->output->single_button(
+                    new moodle_url('/mod/folder/download_folder.php', array('id' => $cm->id)),
+                    get_string('downloadfolder', 'folder')
+                );
+            }
+
+            if (has_capability('mod/folder:managefiles', $context)) {
+                $containercontents .= $this->output->single_button(
+                    new moodle_url('/mod/folder/edit.php', array('id' => $cm->id)),
+                    get_string('edit')
+                );
+            }
             $output .= $this->output->container(
-                    $this->output->single_button(new moodle_url('/mod/folder/edit.php',
-                    array('id' => $cm->id)), get_string('edit')),
-                    'mdl-align folder-edit-button');
+                $containercontents,
+                'mdl-align folder-edit-button');
         }
         return $output;
     }
index f07e58d..e65c4e4 100644 (file)
@@ -28,6 +28,10 @@ defined('MOODLE_INTERNAL') || die;
 if ($ADMIN->fulltree) {
     //--- general settings -----------------------------------------------------------------------------------
     $settings->add(new admin_setting_configcheckbox('folder/showexpanded',
-            get_string('showexpanded', 'folder'),
-            get_string('showexpanded_help', 'folder'), 1));
+        get_string('showexpanded', 'folder'),
+        get_string('showexpanded_help', 'folder'), 1));
+
+    $settings->add(new admin_setting_configtext('folder/maxsizetodownload',
+        get_string('maxsizetodownload', 'folder'),
+        get_string('maxsizetodownload_help', 'folder'), '', PARAM_INT));
 }
index cd48911..5bfa7f7 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015111600;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2016020201;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015111000;    // Requires this Moodle version
 $plugin->component = 'mod_folder';     // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index f195b01..1733de0 100644 (file)
  */
 
 $messageproviders = array (
+    // Ordinary single forum posts.
+    'posts' => array(
+    ),
 
-/// Ordinary single forum posts
-    'posts' => array (
-    )
-
+    // Forum digest messages.
+    'digests' => array(
+    ),
 );
-
-
-
index ae32f1e..f00cc11 100644 (file)
@@ -461,6 +461,7 @@ $string['shortpost'] = 'Short post';
 $string['showsubscribers'] = 'Show/edit current subscribers';
 $string['singleforum'] = 'A single simple discussion';
 $string['smallmessage'] = '{$a->user} posted in {$a->forumname}';
+$string['smallmessagedigest'] = 'Forum digest containing {$a} messages';
 $string['startedby'] = 'Started by';
 $string['subject'] = 'Subject';
 $string['subscribe'] = 'Subscribe to this forum';
index d993c41..121b2e6 100644 (file)
@@ -1063,6 +1063,7 @@ function forum_cron() {
 
                     $postsarray = $discussionposts[$discussionid];
                     sort($postsarray);
+                    $sentcount = 0;
 
                     foreach ($postsarray as $postid) {
                         $post = $posts[$postid];
@@ -1144,6 +1145,7 @@ function forum_cron() {
                                 $userto->markposts[$post->id] = $post->id;
                             }
                         }
+                        $sentcount++;
                     }
                     $footerlinks = array();
                     if ($canunsubscribe) {
@@ -1162,10 +1164,18 @@ function forum_cron() {
                     $posthtml = '';
                 }
 
-                $attachment = $attachname='';
-                // Directly email forum digests rather than sending them via messaging, use the
-                // site shortname as 'from name', the noreply address will be used by email_to_user.
-                $mailresult = email_to_user($userto, $site->shortname, $postsubject, $posttext, $posthtml, $attachment, $attachname);
+                $eventdata = new \core\message\message();
+                $eventdata->component           = 'mod_forum';
+                $eventdata->name                = 'digests';
+                $eventdata->userfrom            = core_user::get_noreply_user();
+                $eventdata->userto              = $userto;
+                $eventdata->subject             = $postsubject;
+                $eventdata->fullmessage         = $posttext;
+                $eventdata->fullmessageformat   = FORMAT_PLAIN;
+                $eventdata->fullmessagehtml     = $posthtml;
+                $eventdata->notification        = 1;
+                $eventdata->smallmessage        = get_string('smallmessagedigest', 'forum', $sentcount);
+                $mailresult = message_send($eventdata);
 
                 if (!$mailresult) {
                     mtrace("ERROR: mod/forum/cron.php: Could not send out digest mail to user $userto->id ".
index f354b23..a30885d 100644 (file)
 {{/ showdiscussionname }}
 </div>
 
-<table border="0" cellpadding="3" cellspacing="0" class="forumpost">
-    <tr class="header">
-        <td width="35" valign="top" class="picture left">
-            {{{ authorpicture }}}
-        </td>
-        <td class="topic {{# firstpost }}starter{{/ firstpost }}">
-            <div class="subject">
-                {{{ subject }}}
-            </div>
-            <div class="author">
-                {{# str }} bynameondate, forum, { "name": "<a target='_blank' href='{{{ authorlink }}}'>{{ authorfullname }}</a>", "date": "{{ postdate }}" } {{/ str }}
-            </div>
-        </td>
-    </tr>
-    <tr>
-        <td class="left side" valign="top">
-            {{# grouppicture }}
-                {{{ grouppicture }}}
-            {{/ grouppicture }}
-            {{^ grouppicture }}
-                &nbsp;
-            {{/ grouppicture }}
-        </td>
-        <td class="content">
-            {{# attachments }}
-                <div class="attachments">
-                    {{{ attachments }}}
-                </div>
-            {{/ attachments }}
-            {{{ message }}}
-
-            <div class="commands">
-                {{^ firstpost }}
-                    <a target="_blank" href="{{{ parentpostlink }}}">
-                        {{# str }} parent, forum {{/ str }}
-                    </a>
-                    {{# canreply }}
-                        |
-                    {{/ canreply }}
-                {{/ firstpost }}
-                {{# canreply }}
-                    <a target="_blank" href="{{{ replylink }}}">
-                        {{# str }} reply, forum {{/ str }}
-                    </a>
-                {{/ canreply }}
-            </div>
-
-            <div class="link">
-                <a target="_blank" href="{{{ permalink }}}">
-                    {{# str }} postincontext, forum {{/ str }}
-                </a>
-            </div>
-        </td>
-    </tr>
-</table>
+{{> mod_forum/forum_post_email_htmlemail_body }}
 
 <hr />
 <div class="mdl-align unsubscribelink">
diff --git a/mod/forum/templates/forum_post_email_htmlemail_body.mustache b/mod/forum/templates/forum_post_email_htmlemail_body.mustache
new file mode 100644 (file)
index 0000000..3798903
--- /dev/null
@@ -0,0 +1,134 @@
+{{!
+    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/>.
+}}
+{{!
+    @template mod_forum/forum_post_emaildigestfull_htmlemail_body
+
+    Template which defines the body component of a forum post for sending in a single-post HTML email.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * courselink
+    * coursename
+    * forumindexlink
+    * forumviewlink
+    * forumname
+    * discussionlink
+    * discussionname
+    * showdiscussionname
+    * firstpost
+    * subject
+    * authorlink
+    * authorpicture
+    * authorfullname
+    * postdate
+    * grouppicture
+    * attachments
+    * message
+    * parentpostlink
+    * canreply
+    * replylink
+    * permalink
+    * unsubscribeforumlink
+    * unsubscribediscussionlink
+
+    Example context (json):
+    {
+        "courselink": "https://example.com/course/view.php?id=2",
+        "coursename": "Example course",
+        "forumindexlink": "https://example.com/mod/forum/index.php?id=2",
+        "forumviewlink": "https://example.com/mod/forum/view.php?f=2",
+        "forumname": "Lorem ipsum dolor",
+        "discussionlink": "https://example.com/mod/forum/discuss.php?d=70",
+        "discussionname": "Is Lorem ipsum Latin?",
+        "showdiscussionname": 1,
+        "firstpost": 1,
+        "subject": "Is Lorem ipsum Latin?",
+        "authorlink": "https://example.com/user/view.php?id=2&course=2",
+        "authorpicture": "<a href=\"https://example.com/user/view.php?id=2&amp;course=6\"><img src=\"https://example.com/theme/image.php?theme=clean&amp;component=core&amp;image=u%2Ff2&amp;svg=0\" alt=\"Picture of Admin User\" title=\"Picture of Admin User\" class=\"userpicture defaultuserpic\" width=\"35\" height=\"35\" /></a>",
+        "authorfullname": "Lucius Caecilius lucundus",
+        "postdate": "Sunday, 13 September 2015, 2:22 pm",
+        "grouppicture": "",
+        "attachments": "",
+        "message": "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum et auctor libero. Quisque porta egestas neque, et bibendum libero dignissim at. Nulla facilisi. Morbi eget accumsan felis. Nunc et vulputate odio, vel venenatis nisl. Nunc maximus ipsum sed tincidunt mollis. Integer nunc erat, luctus sit amet arcu tincidunt, volutpat dignissim mi. Sed ut magna quam.  Mauris accumsan porta turpis sed aliquam. Etiam at justo tristique, imperdiet augue quis, consectetur sapien. Ut nec erat malesuada sem suscipit lobortis. Vivamus posuere nibh eu ipsum porta fringilla.  Sed vitae dapibus ipsum, ac condimentum enim. Sed dignissim ante at elit mollis, ac tempor lacus iaculis. Etiam nec lectus vitae nibh vulputate volutpat. Nulla quis tellus aliquam, commodo nisi et, dictum est.</p><p><br /></p>",
+        "parentpostlink": "",
+        "canreply": 1,
+        "replylink": "https://example.com/mod/forum/post.php?reply=2",
+        "permalink": "https://example.com/mod/forum/discuss.php?d=2#2",
+        "unsubscribeforumlink": "https://example.com/mod/forum/subscribe.php?id=2",
+        "unsubscribediscussionlink": "https://example.com/mod/discussion/subscribe.php?id=2&d=2"
+    }
+}}
+<table border="0" cellpadding="3" cellspacing="0" class="forumpost">
+    <tr class="header">
+        <td width="35" valign="top" class="picture left">
+            {{{ authorpicture }}}
+        </td>
+        <td class="topic {{# firstpost }}starter{{/ firstpost }}">
+            <div class="subject">
+                {{{ subject }}}
+            </div>
+            <div class="author">
+                {{# str }} bynameondate, forum, { "name": "<a target='_blank' href='{{{ authorlink }}}'>{{ authorfullname }}</a>", "date": "{{ postdate }}" } {{/ str }}
+            </div>
+        </td>
+    </tr>
+    <tr>
+        <td class="left side" valign="top">
+            {{# grouppicture }}
+                {{{ grouppicture }}}
+            {{/ grouppicture }}
+            {{^ grouppicture }}
+                &nbsp;
+            {{/ grouppicture }}
+        </td>
+        <td class="content">
+            {{# attachments }}
+                <div class="attachments">
+                    {{{ attachments }}}
+                </div>
+            {{/ attachments }}
+            {{{ message }}}
+
+            <div class="commands">
+                {{^ firstpost }}
+                    <a target="_blank" href="{{{ parentpostlink }}}">
+                        {{# str }} parent, forum {{/ str }}
+                    </a>
+                    {{# canreply }}
+                        |
+                    {{/ canreply }}
+                {{/ firstpost }}
+                {{# canreply }}
+                    <a target="_blank" href="{{{ replylink }}}">
+                        {{# str }} reply, forum {{/ str }}
+                    </a>
+                {{/ canreply }}
+            </div>
+
+            <div class="link">
+                <a target="_blank" href="{{{ permalink }}}">
+                    {{# str }} postincontext, forum {{/ str }}
+                </a>
+            </div>
+        </td>
+    </tr>
+</table>
index 5b2240f..74dc589 100644 (file)
@@ -77,4 +77,4 @@
         "unsubscribediscussionlink": "https://example.com/mod/discussion/subscribe.php?id=2&d=2"
     }
 }}
-{{> mod_forum/forum_post_email_htmlemail }}
+{{> mod_forum/forum_post_email_htmlemail_body }}
index 12d24bb..b694007 100644 (file)
@@ -150,9 +150,10 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
      * specified number of times.
      *
      * @param integer $expected The number of times that the post should have been sent
-     * @return array An array of the messages caught by the message sink
+     * @param integer $individualcount The number of individual messages sent
+     * @param integer $digestcount The number of digest messages sent
      */
-    protected function helper_run_cron_check_count($expected, $messagecount, $mailcount) {
+    protected function helper_run_cron_check_count($expected, $individualcount, $digestcount) {
         if ($expected === 0) {
             $this->expectOutputRegex('/(Email digests successfully sent to .* users.){0}/');
         } else {
@@ -162,15 +163,18 @@ class mod_forum_maildigest_testcase extends advanced_testcase {
 
         // Now check the results in the message sink.
         $messages = $this->helper->messagesink->get_messages();
-        // There should be the expected number of messages.
-        $this->assertEquals($messagecount, count($messages));
 
-        // Now check the results in the mail sink.
-        $messages = $this->helper->mailsink->get_messages();
-        // There should be the expected number of messages.
-        $this->assertEquals($mailcount, count($messages));
+        $counts = (object) array('digest' => 0, 'individual' => 0);
+        foreach ($messages as $message) {
+            if (strpos($message->subject, 'forum digest') !== false) {
+                $counts->digest++;
+            } else {
+                $counts->individual++;
+            }
+        }
 
-        return $messages;
+        $this->assertEquals($digestcount, $counts->digest);
+        $this->assertEquals($individualcount, $counts->individual);
     }
 
     public function test_set_maildigest() {
index 50277d1..cd4eaf1 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015120800;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2015120801;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015111000;       // Requires this Moodle version
 $plugin->component = 'mod_forum';      // Full name of the plugin (used for diagnostics)
index 66d366d..dad4a8b 100644 (file)
              }
          }
 
-         echo "<p <p class=\"centerpara\">";
+         echo "<p class=\"centerpara\">";
          echo $OUTPUT->user_picture($user, array('courseid'=>$course->id));
          echo "</p>";
 
 
          if ($showscales) {
              // Print overall summary
-             echo "<p <p class=\"centerpara\">>";
+            echo "<p class=\"centerpara\">";
              survey_print_graph("id=$id&amp;sid=$student&amp;type=student.png");
              echo "</p>";
 
                     $table = new html_table();
                      $table->head = array(get_string($question->text, "survey"));
                      $table->align = array ("left");
-                     $table->data[] = array(s($answer->answer1)); // no html here, just plain text
+                    if (!empty($question->options) && $answer->answer1 > 0) {
+                        $answers = explode(',', get_string($question->options, 'survey'));
+                        if ($answer->answer1 <= count($answers)) {
+                            $table->data[] = array(s($answers[$answer->answer1 - 1])); // No html here, just plain text.
+                        } else {
+                            $table->data[] = array(s($answer->answer1)); // No html here, just plain text.
+                        }
+                    } else {
+                         $table->data[] = array(s($answer->answer1)); // No html here, just plain text.
+                    }
                      echo html_writer::table($table);
                      echo $OUTPUT->spacer(array('height'=>30));
                  }
index 5899b8c..87931c4 100644 (file)
@@ -44,14 +44,14 @@ Feature: Edited wiki pages handle tags correctly
     Then I should see "Cool" in the ".form-autocomplete-selection" "css_element"
     And I press "Cancel"
 
-  Scenario: Wiki page edition of official tags works as expected
+  Scenario: Wiki page edition of standard tags works as expected
     Given I log in as "admin"
     And I expand "Site administration" node
     And I expand "Appearance" node
     And I follow "Manage tags"
     And I follow "Default collection"
     And I set the field "otagsadd" to "OT1, OT2, OT3"
-    And I press "Add official tags"
+    And I press "Add standard tags"
     And I log out
     And I log in as "student1"
     And I follow "Course 1"
index cd63c07..c2194e3 100644 (file)
 {
   "name": "Moodle",
   "dependencies": {
-    "grunt": {
-      "version": "0.4.5",
-      "from": "grunt@0.4.5",
-      "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz",
+    "abbrev": {
+      "version": "1.0.7",
+      "from": "abbrev@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.7.tgz"
+    },
+    "align-text": {
+      "version": "0.1.3",
+      "from": "align-text@>=0.1.0 <0.2.0",
+      "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.3.tgz"
+    },
+    "amdefine": {
+      "version": "1.0.0",
+      "from": "amdefine@>=0.0.4",
+      "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz"
+    },
+    "ansi-color": {
+      "version": "0.2.1",
+      "from": "ansi-color@*",
+      "resolved": "https://registry.npmjs.org/ansi-color/-/ansi-color-0.2.1.tgz"
+    },
+    "ansi-regex": {
+      "version": "2.0.0",
+      "from": "ansi-regex@>=2.0.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz"
+    },
+    "ansi-styles": {
+      "version": "2.1.0",
+      "from": "ansi-styles@>=2.1.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.1.0.tgz"
+    },
+    "argparse": {
+      "version": "0.1.16",
+      "from": "argparse@>=0.1.11 <0.2.0",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz",
+      "dependencies": {
+        "underscore.string": {
+          "version": "2.4.0",
+          "from": "underscore.string@>=2.4.0 <2.5.0",
+          "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz"
+        }
+      }
+    },
+    "asap": {
+      "version": "1.0.0",
+      "from": "asap@>=1.0.0 <1.1.0",
+      "resolved": "https://registry.npmjs.org/asap/-/asap-1.0.0.tgz"
+    },
+    "asn1": {
+      "version": "0.2.3",
+      "from": "asn1@>=0.2.3 <0.3.0",
+      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz"
+    },
+    "assert-plus": {
+      "version": "0.1.5",
+      "from": "assert-plus@>=0.1.5 <0.2.0",
+      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz"
+    },
+    "async": {
+      "version": "1.5.2",
+      "from": "async@*",
+      "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz"
+    },
+    "aws-sign2": {
+      "version": "0.6.0",
+      "from": "aws-sign2@>=0.6.0 <0.7.0",
+      "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz"
+    },
+    "balanced-match": {
+      "version": "0.3.0",
+      "from": "balanced-match@>=0.3.0 <0.4.0",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.3.0.tgz"
+    },
+    "bl": {
+      "version": "1.0.0",
+      "from": "bl@>=1.0.0 <1.1.0",
+      "resolved": "https://registry.npmjs.org/bl/-/bl-1.0.0.tgz",
+      "dependencies": {
+        "readable-stream": {
+          "version": "2.0.5",
+          "from": "readable-stream@>=2.0.0 <2.1.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.5.tgz"
+        }
+      }
+    },
+    "boom": {
+      "version": "2.10.1",
+      "from": "boom@>=2.0.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz"
+    },
+    "brace-expansion": {
+      "version": "1.1.2",
+      "from": "brace-expansion@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.2.tgz"
+    },
+    "browserify-zlib": {
+      "version": "0.1.4",
+      "from": "browserify-zlib@>=0.1.4 <0.2.0",
+      "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz"
+    },
+    "builtin-modules": {
+      "version": "1.1.1",
+      "from": "builtin-modules@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz"
+    },
+    "camelcase": {
+      "version": "2.0.1",
+      "from": "camelcase@>=2.0.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.0.1.tgz"
+    },
+    "camelcase-keys": {
+      "version": "2.0.0",
+      "from": "camelcase-keys@>=2.0.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.0.0.tgz"
+    },
+    "caseless": {
+      "version": "0.11.0",
+      "from": "caseless@>=0.11.0 <0.12.0",
+      "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz"
+    },
+    "center-align": {
+      "version": "0.1.2",
+      "from": "center-align@>=0.1.1 <0.2.0",
+      "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.2.tgz"
+    },
+    "chalk": {
+      "version": "1.1.1",
+      "from": "chalk@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.1.tgz"
+    },
+    "cli": {
+      "version": "0.6.6",
+      "from": "cli@>=0.6.0 <0.7.0",
+      "resolved": "https://registry.npmjs.org/cli/-/cli-0.6.6.tgz",
       "dependencies": {
-        "async": {
-          "version": "0.1.22",
-          "from": "async@>=0.1.22 <0.2.0",
-          "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz"
-        },
-        "coffee-script": {
-          "version": "1.3.3",
-          "from": "coffee-script@>=1.3.3 <1.4.0",
-          "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz"
-        },
-        "colors": {
-          "version": "0.6.2",
-          "from": "colors@>=0.6.2 <0.7.0",
-          "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz"
-        },
-        "dateformat": {
-          "version": "1.0.2-1.2.3",
-          "from": "dateformat@1.0.2-1.2.3",
-          "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz"
-        },
-        "eventemitter2": {
-          "version": "0.4.14",
-          "from": "eventemitter2@>=0.4.13 <0.5.0",
-          "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz"
-        },
-        "findup-sync": {
-          "version": "0.1.3",
-          "from": "findup-sync@>=0.1.2 <0.2.0",
-          "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz",
-          "dependencies": {
-            "glob": {
-              "version": "3.2.11",
-              "from": "glob@>=3.2.9 <3.3.0",
-              "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz",
-              "dependencies": {
-                "inherits": {
-                  "version": "2.0.1",
-                  "from": "inherits@>=2.0.0 <3.0.0",
-                  "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
-                },
-                "minimatch": {
-                  "version": "0.3.0",
-                  "from": "minimatch@>=0.3.0 <0.4.0",
-                  "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz",
-                  "dependencies": {
-                    "lru-cache": {
-                      "version": "2.7.3",
-                      "from": "lru-cache@>=2.0.0 <3.0.0",
-                      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz"
-                    },
-                    "sigmund": {
-                      "version": "1.0.1",
-                      "from": "sigmund@>=1.0.0 <1.1.0",
-                      "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz"
-                    }
-                  }
-                }
-              }
-            },
-            "lodash": {
-              "version": "2.4.2",
-              "from": "lodash@>=2.4.1 <2.5.0",
-              "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz"
-            }
-          }
-        },
         "glob": {
-          "version": "3.1.21",
-          "from": "glob@>=3.1.21 <3.2.0",
-          "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz",
-          "dependencies": {
-            "graceful-fs": {
-              "version": "1.2.3",
-              "from": "graceful-fs@>=1.2.0 <1.3.0",
-              "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz"
-            },
-            "inherits": {
-              "version": "1.0.2",
-              "from": "inherits@>=1.0.0 <2.0.0",
-              "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz"
-            }
-          }
-        },
-        "hooker": {
-          "version": "0.2.3",
-          "from": "hooker@>=0.2.3 <0.3.0",
-          "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz"
-        },
-        "iconv-lite": {
-          "version": "0.2.11",
-          "from": "iconv-lite@>=0.2.11 <0.3.0",
-          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz"
+          "version": "3.2.11",
+          "from": "glob@>=3.2.1 <3.3.0",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz"
         },
         "minimatch": {
-          "version": "0.2.14",
-          "from": "minimatch@>=0.2.12 <0.3.0",
-          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz",
-          "dependencies": {
-            "lru-cache": {
-              "version": "2.7.3",
-              "from": "lru-cache@>=2.0.0 <3.0.0",
-              "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz"
-            },
-            "sigmund": {
-              "version": "1.0.1",
-              "from": "sigmund@>=1.0.0 <1.1.0",
-              "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz"
-            }
-          }
+          "version": "0.3.0",
+          "from": "minimatch@>=0.3.0 <0.4.0",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz"
+        }
+      }
+    },
+    "cliui": {
+      "version": "2.1.0",
+      "from": "cliui@>=2.1.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz"
+    },
+    "coffee-script": {
+      "version": "1.3.3",
+      "from": "coffee-script@>=1.3.3 <1.4.0",
+      "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz"
+    },
+    "colors": {
+      "version": "0.6.2",
+      "from": "colors@>=0.6.2 <0.7.0",
+      "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz"
+    },
+    "combined-stream": {
+      "version": "1.0.5",
+      "from": "combined-stream@>=1.0.5 <1.1.0",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz"
+    },
+    "commander": {
+      "version": "2.9.0",
+      "from": "commander@>=2.9.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz"
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "from": "concat-map@0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
+    },
+    "concat-stream": {
+      "version": "1.5.1",
+      "from": "concat-stream@>=1.4.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.1.tgz",
+      "dependencies": {
+        "readable-stream": {
+          "version": "2.0.5",
+          "from": "readable-stream@>=2.0.0 <2.1.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.5.tgz"
+        }
+      }
+    },
+    "console-browserify": {
+      "version": "1.1.0",
+      "from": "console-browserify@>=1.1.0 <1.2.0",
+      "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz"
+    },
+    "core-util-is": {
+      "version": "1.0.2",
+      "from": "core-util-is@>=1.0.0 <1.1.0",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
+    },
+    "cpr": {
+      "version": "0.0.6",
+      "from": "cpr@>=0.0.6 <0.1.0",
+      "resolved": "https://registry.npmjs.org/cpr/-/cpr-0.0.6.tgz",
+      "dependencies": {
+        "graceful-fs": {
+          "version": "1.1.14",
+          "from": "graceful-fs@>=1.1.14 <1.2.0",
+          "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.1.14.tgz"
         },
-        "nopt": {
-          "version": "1.0.10",
-          "from": "nopt@>=1.0.10 <1.1.0",
-          "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
-          "dependencies": {
-            "abbrev": {
-              "version": "1.0.7",
-              "from": "abbrev@>=1.0.0 <2.0.0",
-              "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.7.tgz"
-            }
-          }
+        "mkdirp": {
+          "version": "0.3.5",
+          "from": "mkdirp@>=0.3.4 <0.4.0",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz"
         },
         "rimraf": {
-          "version": "2.2.8",
-          "from": "rimraf@>=2.2.8 <2.3.0",
-          "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz"
+          "version": "2.0.3",
+          "from": "rimraf@>=2.0.2 <2.1.0",
+          "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.0.3.tgz"
+        }
+      }
+    },
+    "cryptiles": {
+      "version": "2.0.5",
+      "from": "cryptiles@>=2.0.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz"
+    },
+    "csslint": {
+      "version": "0.10.0",
+      "from": "csslint@>=0.10.0 <0.11.0",
+      "resolved": "https://registry.npmjs.org/csslint/-/csslint-0.10.0.tgz"
+    },
+    "cssproc": {
+      "version": "0.0.7",
+      "from": "cssproc@>=0.0.1 <0.1.0",
+      "resolved": "https://registry.npmjs.org/cssproc/-/cssproc-0.0.7.tgz"
+    },
+    "dashdash": {
+      "version": "1.12.1",
+      "from": "dashdash@>=1.10.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.12.1.tgz"
+    },
+    "date-now": {
+      "version": "0.1.4",
+      "from": "date-now@>=0.1.4 <0.2.0",
+      "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz"
+    },
+    "dateformat": {
+      "version": "1.0.2-1.2.3",
+      "from": "dateformat@1.0.2-1.2.3",
+      "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz"
+    },
+    "debug": {
+      "version": "0.7.4",
+      "from": "debug@>=0.7.0 <0.8.0",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz"
+    },
+    "decamelize": {
+      "version": "1.1.2",
+      "from": "decamelize@>=1.1.2 <2.0.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.1.2.tgz"
+    },
+    "delayed-stream": {
+      "version": "1.0.0",
+      "from": "delayed-stream@>=1.0.0 <1.1.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
+    },
+    "dom-serializer": {
+      "version": "0.1.0",
+      "from": "dom-serializer@>=0.0.0 <1.0.0",
+      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
+      "dependencies": {
+        "domelementtype": {
+          "version": "1.1.3",
+          "from": "domelementtype@>=1.1.1 <1.2.0",
+          "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz"
+        },
+        "entities": {
+          "version": "1.1.1",
+          "from": "entities@>=1.1.1 <1.2.0",
+          "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz"
+        }
+      }
+    },
+    "domelementtype": {
+      "version": "1.3.0",
+      "from": "domelementtype@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz"
+    },
+    "domhandler": {
+      "version": "2.3.0",
+      "from": "domhandler@>=2.3.0 <2.4.0",
+      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz"
+    },
+    "domutils": {
+      "version": "1.5.1",
+      "from": "domutils@>=1.5.0 <1.6.0",
+      "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz"
+    },
+    "ecc-jsbn": {
+      "version": "0.1.1",
+      "from": "ecc-jsbn@>=0.0.1 <1.0.0",
+      "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz"
+    },
+    "entities": {
+      "version": "1.0.0",
+      "from": "entities@>=1.0.0 <1.1.0",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz"
+    },
+    "errno": {
+      "version": "0.1.4",
+      "from": "errno@>=0.1.1 <0.2.0",
+      "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.4.tgz"
+    },
+    "error-ex": {
+      "version": "1.3.0",
+      "from": "error-ex@>=1.2.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.0.tgz"
+    },
+    "escape-string-regexp": {
+      "version": "1.0.4",
+      "from": "escape-string-regexp@>=1.0.2 <2.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.4.tgz"
+    },
+    "escodegen": {
+      "version": "0.0.28",
+      "from": "escodegen@>=0.0.0 <0.1.0",
+      "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-0.0.28.tgz"
+    },
+    "esprima": {
+      "version": "1.0.4",
+      "from": "esprima@>=1.0.2 <1.1.0",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz"
+    },
+    "estraverse": {
+      "version": "1.3.2",
+      "from": "estraverse@>=1.3.0 <1.4.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.3.2.tgz"
+    },
+    "eventemitter2": {
+      "version": "0.4.14",
+      "from": "eventemitter2@>=0.4.13 <0.5.0",
+      "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz"
+    },
+    "exit": {
+      "version": "0.1.2",
+      "from": "exit@>=0.1.1 <0.2.0",
+      "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz"
+    },
+    "extend": {
+      "version": "3.0.0",
+      "from": "extend@>=3.0.0 <3.1.0",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz"
+    },
+    "extsprintf": {
+      "version": "1.0.2",
+      "from": "extsprintf@1.0.2",
+      "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz"
+    },
+    "faye-websocket": {
+      "version": "0.4.4",
+      "from": "faye-websocket@>=0.4.3 <0.5.0",
+      "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.4.4.tgz"
+    },
+    "figures": {
+      "version": "1.4.0",
+      "from": "figures@>=1.0.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/figures/-/figures-1.4.0.tgz"
+    },
+    "fileset": {
+      "version": "0.1.8",
+      "from": "fileset@>=0.1.0 <0.2.0",
+      "resolved": "https://registry.npmjs.org/fileset/-/fileset-0.1.8.tgz"
+    },
+    "find-up": {
+      "version": "1.1.0",
+      "from": "find-up@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.0.tgz"
+    },
+    "findup-sync": {
+      "version": "0.1.3",
+      "from": "findup-sync@>=0.1.2 <0.2.0",
+      "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz",
+      "dependencies": {
+        "glob": {
+          "version": "3.2.11",
+          "from": "glob@>=3.2.9 <3.3.0",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz"
         },
         "lodash": {
-          "version": "0.9.2",
-          "from": "lodash@>=0.9.2 <0.10.0",
-          "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz"
+          "version": "2.4.2",
+          "from": "lodash@>=2.4.1 <2.5.0",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz"
         },
-        "underscore.string": {
-          "version": "2.2.1",
-          "from": "underscore.string@>=2.2.1 <2.3.0",
-          "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz"
+        "minimatch": {
+          "version": "0.3.0",
+          "from": "minimatch@>=0.3.0 <0.4.0",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz"
+        }
+      }
+    },
+    "forever-agent": {
+      "version": "0.6.1",
+      "from": "forever-agent@>=0.6.1 <0.7.0",
+      "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz"
+    },
+    "form-data": {
+      "version": "1.0.0-rc3",
+      "from": "form-data@>=1.0.0-rc3 <1.1.0",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc3.tgz",
+      "dependencies": {
+        "async": {
+          "version": "1.5.2",
+          "from": "async@>=1.4.0 <2.0.0",
+          "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz"
+        }
+      }
+    },
+    "gaze": {
+      "version": "0.5.2",
+      "from": "gaze@>=0.5.1 <0.6.0",
+      "resolved": "https://registry.npmjs.org/gaze/-/gaze-0.5.2.tgz"
+    },
+    "gear": {
+      "version": "0.8.18",
+      "from": "gear@>=0.8.0 <0.9.0",
+      "resolved": "https://registry.npmjs.org/gear/-/gear-0.8.18.tgz",
+      "dependencies": {
+        "async": {
+          "version": "0.2.10",
+          "from": "async@>=0.2.0 <0.3.0",
+          "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz"
         },
-        "which": {
-          "version": "1.0.9",
-          "from": "which@>=1.0.5 <1.1.0",
-          "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz"
+        "mkdirp": {
+          "version": "0.3.5",
+          "from": "mkdirp@>=0.3.0 <0.4.0",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz"
+        }
+      }
+    },
+    "gear-lib": {
+      "version": "0.8.15",
+      "from": "gear-lib@>=0.8.0 <0.9.0",
+      "resolved": "https://registry.npmjs.org/gear-lib/-/gear-lib-0.8.15.tgz",
+      "dependencies": {
+        "async": {
+          "version": "0.2.10",
+          "from": "async@>=0.2.0 <0.3.0",
+          "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz"
         },
-        "js-yaml": {
-          "version": "2.0.5",
-          "from": "js-yaml@>=2.0.5 <2.1.0",
-          "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz",
+        "glob": {
+          "version": "3.2.11",
+          "from": "glob@>=3.2.0 <3.3.0",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz"
+        },
+        "jshint": {
+          "version": "2.5.11",
+          "from": "jshint@>=2.5.0 <2.6.0",
+          "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.5.11.tgz",
           "dependencies": {
-            "argparse": {
-              "version": "0.1.16",
-              "from": "argparse@>=0.1.11 <0.2.0",
-              "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz",
-              "dependencies": {
-                "underscore": {
-                  "version": "1.7.0",
-                  "from": "underscore@>=1.7.0 <1.8.0",
-                  "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz"
-                },
-                "underscore.string": {
-                  "version": "2.4.0",
-                  "from": "underscore.string@>=2.4.0 <2.5.0",
-                  "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz"
-                }
-              }
-            },
-            "esprima": {
-              "version": "1.0.4",
-              "from": "esprima@>=1.0.2 <1.1.0",
-              "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz"
+            "minimatch": {
+              "version": "1.0.0",
+              "from": "minimatch@>=1.0.0 <1.1.0",
+              "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-1.0.0.tgz"
             }
           }
         },
-        "exit": {
-          "version": "0.1.2",
-          "from": "exit@>=0.1.1 <0.2.0",
-          "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz"
+        "less": {
+          "version": "1.3.3",
+          "from": "less@>=1.3.0 <1.4.0",
+          "resolved": "https://registry.npmjs.org/less/-/less-1.3.3.tgz"
         },
-        "getobject": {
-          "version": "0.1.0",
-          "from": "getobject@>=0.1.0 <0.2.0",
-          "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz"
+        "mime": {
+          "version": "1.2.11",
+          "from": "mime@>=1.2.0 <1.3.0",
+          "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz"
         },
-        "grunt-legacy-util": {
-          "version": "0.2.0",
-          "from": "grunt-legacy-util@>=0.2.0 <0.3.0",
-          "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz"
+        "minimatch": {
+          "version": "0.3.0",
+          "from": "minimatch@>=0.3.0 <0.4.0",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz"
         },
-        "grunt-legacy-log": {
-          "version": "0.1.2",
-          "from": "grunt-legacy-log@>=0.1.0 <0.2.0",
-          "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.2.tgz",
-          "dependencies": {
-            "grunt-legacy-log-utils": {
-              "version": "0.1.1",
-              "from": "grunt-legacy-log-utils@>=0.1.1 <0.2.0",
-              "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz"
-            },
-            "lodash": {
-              "version": "2.4.2",
-              "from": "lodash@>=2.4.1 <2.5.0",
-              "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz"
-            },
-            "underscore.string": {
-              "version": "2.3.3",
-              "from": "underscore.string@>=2.3.3 <2.4.0",
-              "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz"
-            }
-          }
+        "uglify-js": {
+          "version": "1.3.5",
+          "from": "uglify-js@>=1.3.0 <1.4.0",
+          "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-1.3.5.tgz"
+        },
+        "underscore": {
+          "version": "1.6.0",
+          "from": "underscore@>=1.6.0 <1.7.0",
+          "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz"
         }
       }
     },
-    "grunt-contrib-jshint": {
-      "version": "0.11.3",
-      "from": "grunt-contrib-jshint@0.11.3",
-      "resolved": "https://registry.npmjs.org/grunt-contrib-jshint/-/grunt-contrib-jshint-0.11.3.tgz",
+    "generate-function": {
+      "version": "2.0.0",
+      "from": "generate-function@>=2.0.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz"
+    },
+    "generate-object-property": {
+      "version": "1.2.0",
+      "from": "generate-object-property@>=1.1.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz"
+    },
+    "get-stdin": {
+      "version": "4.0.1",
+      "from": "get-stdin@>=4.0.1 <5.0.0",
+      "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz"
+    },
+    "getobject": {
+      "version": "0.1.0",
+      "from": "getobject@>=0.1.0 <0.2.0",
+      "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz"
+    },
+    "glob": {
+      "version": "3.1.21",
+      "from": "glob@>=3.1.21 <3.2.0",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz",
       "dependencies": {
-        "hooker": {
-          "version": "0.2.3",
-          "from": "hooker@>=0.2.3 <0.3.0",
-          "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz"
-        },
-        "jshint": {
-          "version": "2.8.0",
-          "from": "jshint@>=2.8.0 <2.9.0",
-          "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.8.0.tgz",
-          "dependencies": {
-            "cli": {
-              "version": "0.6.6",
-              "from": "cli@>=0.6.0 <0.7.0",
-              "resolved": "https://registry.npmjs.org/cli/-/cli-0.6.6.tgz",
-              "dependencies": {
-                "glob": {
-                  "version": "3.2.11",
-                  "from": "glob@>=3.2.1 <3.3.0",
-                  "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz",
-                  "dependencies": {
-                    "inherits": {
-                      "version": "2.0.1",
-                      "from": "inherits@>=2.0.0 <3.0.0",
-                      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
-                    },
-                    "minimatch": {
-                      "version": "0.3.0",
-                      "from": "minimatch@>=0.3.0 <0.4.0",
-                      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz",
-                      "dependencies": {
-                        "lru-cache": {
-                          "version": "2.7.3",
-                          "from": "lru-cache@>=2.0.0 <3.0.0",
-                          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz"
-                        },
-                        "sigmund": {
-                          "version": "1.0.1",
-                          "from": "sigmund@>=1.0.0 <1.1.0",
-                          "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz"
-                        }
-                      }
-                    }
-                  }
-                }
-              }
-            },
-            "console-browserify": {
-              "version": "1.1.0",
-              "from": "console-browserify@>=1.1.0 <1.2.0",
-              "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz",
-              "dependencies": {
-                "date-now": {
-                  "version": "0.1.4",
-                  "from": "date-now@>=0.1.4 <0.2.0",
-                  "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz"
-                }
-              }
-            },
-            "exit": {
-              "version": "0.1.2",
-              "from": "exit@>=0.1.0 <0.2.0",
-              "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz"
-            },
-            "htmlparser2": {
-              "version": "3.8.3",
-              "from": "htmlparser2@>=3.8.0 <3.9.0",
-              "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz",
-              "dependencies": {
-                "domhandler": {
-                  "version": "2.3.0",
-                  "from": "domhandler@>=2.3.0 <2.4.0",
-                  "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz"
-                },
-                "domutils": {
-                  "version": "1.5.1",
-                  "from": "domutils@>=1.5.0 <1.6.0",
-                  "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
-                  "dependencies": {
-                    "dom-serializer": {
-                      "version": "0.1.0",
-                      "from": "dom-serializer@>=0.0.0 <1.0.0",
-                      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
-                      "dependencies": {
-                        "domelementtype": {
-                          "version": "1.1.3",
-                          "from": "domelementtype@>=1.1.1 <1.2.0",
-                          "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz"
-                        },
-                        "entities": {
-                          "version": "1.1.1",
-                          "from": "entities@>=1.1.1 <1.2.0",
-                          "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz"
-                        }
-                      }
-                    }
-                  }
-                },
-                "domelementtype": {
-                  "version": "1.3.0",
-                  "from": "domelementtype@>=1.0.0 <2.0.0",
-                  "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz"
-                },
-                "readable-stream": {
-                  "version": "1.1.13",
-                  "from": "readable-stream@>=1.1.0 <1.2.0",
-                  "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.13.tgz",
-                  "dependencies": {
-                    "core-util-is": {
-                      "version": "1.0.2",
-                      "from": "core-util-is@>=1.0.0 <1.1.0",
-                      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
-                    },
-                    "isarray": {
-                      "version": "0.0.1",
-                      "from": "isarray@0.0.1",
-                      "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
-                    },
-                    "string_decoder": {
-                      "version": "0.10.31",
-                      "from": "string_decoder@>=0.10.0 <0.11.0",
-                      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
-                    },
-                    "inherits": {
-                      "version": "2.0.1",
-                      "from": "inherits@>=2.0.1 <2.1.0",
-                      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
-                    }
-                  }
-                },
-                "entities": {
-                  "version": "1.0.0",
-                  "from": "entities@>=1.0.0 <1.1.0",
-                  "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz"
-                }
-              }
-            },
-            "minimatch": {
-              "version": "2.0.10",
-              "from": "minimatch@>=2.0.0 <2.1.0",
-              "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz",
-              "dependencies": {
-                "brace-expansion": {
-                  "version": "1.1.2",
-                  "from": "brace-expansion@>=1.0.0 <2.0.0",
-                  "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.2.tgz",
-                  "dependencies": {
-                    "balanced-match": {
-                      "version": "0.3.0",
-                      "from": "balanced-match@>=0.3.0 <0.4.0",
-                      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.3.0.tgz"
-                    },
-                    "concat-map": {
-                      "version": "0.0.1",
-                      "from": "concat-map@0.0.1",
-                      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
-                    }
-                  }
-                }
-              }
-            },
-            "shelljs": {
-              "version": "0.3.0",
-              "from": "shelljs@>=0.3.0 <0.4.0",
-              "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz"
-            },
-            "strip-json-comments": {
-              "version": "1.0.4",
-              "from": "strip-json-comments@>=1.0.0 <1.1.0",
-              "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz"
-            },
-            "lodash": {
-              "version": "3.7.0",
-              "from": "lodash@>=3.7.0 <3.8.0",
-              "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.7.0.tgz"
-            }
-          }
+        "inherits": {
+          "version": "1.0.2",
+          "from": "inherits@>=1.0.0 <2.0.0",
+          "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz"
         }
       }
     },
+    "globule": {
+      "version": "0.1.0",
+      "from": "globule@>=0.1.0 <0.2.0",
+      "resolved": "https://registry.npmjs.org/globule/-/globule-0.1.0.tgz",
+      "dependencies": {
+        "lodash": {
+          "version": "1.0.2",
+          "from": "lodash@>=1.0.1 <1.1.0",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.0.2.tgz"
+        }
+      }
+    },
+    "graceful-fs": {
+      "version": "1.2.3",
+      "from": "graceful-fs@>=1.2.0 <1.3.0",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz"
+    },
+    "graceful-readlink": {
+      "version": "1.0.1",
+      "from": "graceful-readlink@>=1.0.0",
+      "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz"
+    },
+    "grunt": {
+      "version": "0.4.5",
+      "from": "grunt@0.4.5",
+      "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz",
+      "dependencies": {
+        "async": {
+          "version": "0.1.22",
+          "from": "async@>=0.1.22 <0.2.0",
+          "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz"
+        }
+      }
+    },
+    "grunt-contrib-jshint": {
+      "version": "0.11.3",
+      "from": "grunt-contrib-jshint@0.11.3",
+      "resolved": "https://registry.npmjs.org/grunt-contrib-jshint/-/grunt-contrib-jshint-0.11.3.tgz"
+    },
     "grunt-contrib-less": {
       "version": "1.1.0",
       "from": "grunt-contrib-less@1.1.0",
           "from": "async@>=0.9.0 <0.10.0",
           "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz"
         },
-        "chalk": {
-          "version": "1.1.1",
-          "from": "chalk@>=1.0.0 <2.0.0",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.1.tgz",
-          "dependencies": {
-            "ansi-styles": {
-              "version": "2.1.0",
-              "from": "ansi-styles@>=2.1.0 <3.0.0",
-              "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.1.0.tgz"
-            },
-            "escape-string-regexp": {
-              "version": "1.0.3",
-              "from": "escape-string-regexp@>=1.0.2 <2.0.0",
-              "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.3.tgz"
-            },
-            "has-ansi": {
-              "version": "2.0.0",
-              "from": "has-ansi@>=2.0.0 <3.0.0",
-              "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
-              "dependencies": {
-                "ansi-regex": {
-                  "version": "2.0.0",
-                  "from": "ansi-regex@>=2.0.0 <3.0.0",
-                  "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz"
-                }
-              }
-            },
-            "strip-ansi": {
-              "version": "3.0.0",
-              "from": "strip-ansi@>=3.0.0 <4.0.0",
-              "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.0.tgz",
-              "dependencies": {
-                "ansi-regex": {
-                  "version": "2.0.0",
-                  "from": "ansi-regex@>=2.0.0 <3.0.0",
-                  "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz"
-                }
-              }
-            },
-            "supports-color": {
-              "version": "2.0.0",
-              "from": "supports-color@>=2.0.0 <3.0.0",
-              "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz"
-            }
-          }
-        },
-        "less": {
-          "version": "2.5.3",
-          "from": "less@>=2.5.0 <2.6.0",
-          "resolved": "https://registry.npmjs.org/less/-/less-2.5.3.tgz",
-          "dependencies": {
-            "errno": {
-              "version": "0.1.4",
-              "from": "errno@>=0.1.1 <0.2.0",
-              "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.4.tgz",
-              "dependencies": {
-                "prr": {
-                  "version": "0.0.0",
-                  "from": "prr@>=0.0.0 <0.1.0",
-                  "resolved": "https://registry.npmjs.org/prr/-/prr-0.0.0.tgz"
-                }
-              }
-            },
-            "graceful-fs": {
-              "version": "3.0.8",
-              "from": "graceful-fs@>=3.0.5 <4.0.0",
-              "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.8.tgz"
-            },
-            "image-size": {
-              "version": "0.3.5",
-              "from": "image-size@>=0.3.5 <0.4.0",
-              "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.3.5.tgz"
-            },
-            "mime": {
-              "version": "1.3.4",
-              "from": "mime@>=1.2.11 <2.0.0",
-              "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz"
-            },
-            "mkdirp": {
-              "version": "0.5.1",
-              "from": "mkdirp@>=0.5.0 <0.6.0",
-              "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
-              "dependencies": {
-                "minimist": {
-                  "version": "0.0.8",
-                  "from": "minimist@0.0.8",
-                  "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz"
-                }
-              }
-            },
-            "promise": {
-              "version": "6.1.0",
-              "from": "promise@>=6.0.1 <7.0.0",
-              "resolved": "https://registry.npmjs.org/promise/-/promise-6.1.0.tgz",
-              "dependencies": {
-                "asap": {
-                  "version": "1.0.0",
-                  "from": "asap@>=1.0.0 <1.1.0",
-                  "resolved": "https://registry.npmjs.org/asap/-/asap-1.0.0.tgz"
-                }
-              }
-            },
-            "request": {
-              "version": "2.67.0",
-              "from": "request@>=2.51.0 <3.0.0",
-              "resolved": "https://registry.npmjs.org/request/-/request-2.67.0.tgz",
-              "dependencies": {
-                "bl": {
-                  "version": "1.0.0",
-                  "from": "bl@>=1.0.0 <1.1.0",
-                  "resolved": "https://registry.npmjs.org/bl/-/bl-1.0.0.tgz",
-                  "dependencies": {
-                    "readable-stream": {
-                      "version": "2.0.4",
-                      "from": "readable-stream@>=2.0.0 <2.1.0",
-                      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.4.tgz",
-                      "dependencies": {
-                        "core-util-is": {
-                          "version": "1.0.2",
-                          "from": "core-util-is@>=1.0.0 <1.1.0",
-                          "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
-                        },
-                        "inherits": {
-                          "version": "2.0.1",
-                          "from": "inherits@>=2.0.1 <2.1.0",
-                          "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
-                        },
-                        "isarray": {
-                          "version": "0.0.1",
-                          "from": "isarray@0.0.1",
-                          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
-                        },
-                        "process-nextick-args": {
-                          "version": "1.0.6",
-                          "from": "process-nextick-args@>=1.0.0 <1.1.0",
-                          "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.6.tgz"
-                        },
-                        "string_decoder": {
-                          "version": "0.10.31",
-                          "from": "string_decoder@>=0.10.0 <0.11.0",
-                          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
-                        },
-                        "util-deprecate": {
-                          "version": "1.0.2",
-                          "from": "util-deprecate@>=1.0.1 <1.1.0",
-                          "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
-                        }
-                      }
-                    }
-                  }
-                },
-                "caseless": {
-                  "version": "0.11.0",
-                  "from": "caseless@>=0.11.0 <0.12.0",
-                  "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz"
-                },
-                "extend": {
-                  "version": "3.0.0",
-                  "from": "extend@>=3.0.0 <3.1.0",
-                  "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz"
-                },
-                "forever-agent": {
-                  "version": "0.6.1",
-                  "from": "forever-agent@>=0.6.1 <0.7.0",
-                  "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz"
-                },
-                "form-data": {
-                  "version": "1.0.0-rc3",
-                  "from": "form-data@>=1.0.0-rc3 <1.1.0",
-                  "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc3.tgz",
-                  "dependencies": {
-                    "async": {
-                      "version": "1.5.0",
-                      "from": "async@>=1.4.0 <2.0.0",
-                      "resolved": "https://registry.npmjs.org/async/-/async-1.5.0.tgz"
-                    }
-                  }
-                },
-                "json-stringify-safe": {
-                  "version": "5.0.1",
-                  "from": "json-stringify-safe@>=5.0.1 <5.1.0",
-                  "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz"
-                },
-                "mime-types": {
-                  "version": "2.1.8",
-                  "from": "mime-types@>=2.1.7 <2.2.0",
-                  "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.8.tgz",
-                  "dependencies": {
-                    "mime-db": {
-                      "version": "1.20.0",
-                      "from": "mime-db@>=1.20.0 <1.21.0",
-                      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.20.0.tgz"
-                    }
-                  }
-                },
-                "node-uuid": {
-                  "version": "1.4.7",
-                  "from": "node-uuid@>=1.4.7 <1.5.0",
-                  "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz"
-                },
-                "qs": {
-                  "version": "5.2.0",
-                  "from": "qs@>=5.2.0 <5.3.0",
-                  "resolved": "https://registry.npmjs.org/qs/-/qs-5.2.0.tgz"
-                },
-                "tunnel-agent": {
-                  "version": "0.4.1",
-                  "from": "tunnel-agent@>=0.4.1 <0.5.0",
-                  "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.1.tgz"
-                },
-                "tough-cookie": {
-                  "version": "2.2.1",
-                  "from": "tough-cookie@>=2.2.0 <2.3.0",
-                  "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.2.1.tgz"
-                },
-                "http-signature": {
-                  "version": "1.1.0",
-                  "from": "http-signature@>=1.1.0 <1.2.0",
-                  "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.0.tgz",
-                  "dependencies": {
-                    "assert-plus": {
-                      "version": "0.1.5",
-                      "from": "assert-plus@>=0.1.5 <0.2.0",
-                      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz"
-                    },
-                    "jsprim": {
-                      "version": "1.2.2",
-                      "from": "jsprim@>=1.2.2 <2.0.0",
-                      "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.2.2.tgz",
-                      "dependencies": {
-                        "extsprintf": {
-                          "version": "1.0.2",
-                          "from": "extsprintf@1.0.2",
-                          "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz"
-                        },
-                        "json-schema": {
-                          "version": "0.2.2",
-                          "from": "json-schema@0.2.2",
-                          "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz"
-                        },
-                        "verror": {
-                          "version": "1.3.6",
-                          "from": "verror@1.3.6",
-                          "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz"
-                        }
-                      }
-                    },
-                    "sshpk": {
-                      "version": "1.7.1",
-                      "from": "sshpk@>=1.7.0 <2.0.0",
-                      "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.7.1.tgz",
-                      "dependencies": {
-                        "asn1": {
-                          "version": "0.2.3",
-                          "from": "asn1@>=0.2.3 <0.3.0",
-                          "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz"
-                        },
-                        "assert-plus": {
-                          "version": "0.2.0",
-                          "from": "assert-plus@>=0.2.0 <0.3.0",
-                          "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz"
-                        },
-                        "dashdash": {
-                          "version": "1.10.1",
-                          "from": "dashdash@>=1.10.1 <2.0.0",
-                          "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.10.1.tgz",
-                          "dependencies": {
-                            "assert-plus": {
-                              "version": "0.1.5",
-                              "from": "assert-plus@>=0.1.0 <0.2.0",
-                              "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz"
-                            }
-                          }
-                        },
-                        "jsbn": {
-                          "version": "0.1.0",
-                          "from": "jsbn@>=0.1.0 <0.2.0",
-                          "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz"
-                        },
-                        "tweetnacl": {
-                          "version": "0.13.2",
-                          "from": "tweetnacl@>=0.13.0 <1.0.0",
-                          "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.13.2.tgz"
-                        },
-                        "jodid25519": {
-                          "version": "1.0.2",
-                          "from": "jodid25519@>=1.0.0 <2.0.0",
-                          "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz"
-                        },
-                        "ecc-jsbn": {
-                          "version": "0.1.1",
-                          "from": "ecc-jsbn@>=0.0.1 <1.0.0",
-                          "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz"
-                        }
-                      }
-                    }
-                  }
-                },
-                "oauth-sign": {
-                  "version": "0.8.0",
-                  "from": "oauth-sign@>=0.8.0 <0.9.0",
-                  "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.0.tgz"
-                },
-                "hawk": {
-                  "version": "3.1.2",
-                  "from": "hawk@>=3.1.0 <3.2.0",
-                  "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.2.tgz",
-                  "dependencies": {
-                    "hoek": {
-                      "version": "2.16.3",
-                      "from": "hoek@>=2.0.0 <3.0.0",
-                      "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz"
-                    },
-                    "boom": {
-                      "version": "2.10.1",
-                      "from": "boom@>=2.0.0 <3.0.0",
-                      "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz"
-                    },
-                    "cryptiles": {
-                      "version": "2.0.5",
-                      "from": "cryptiles@>=2.0.0 <3.0.0",
-                      "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz"
-                    },
-                    "sntp": {
-                      "version": "1.0.9",
-                      "from": "sntp@>=1.0.0 <2.0.0",
-                      "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz"
-                    }
-                  }
-                },
-                "aws-sign2": {
-                  "version": "0.6.0",
-                  "from": "aws-sign2@>=0.6.0 <0.7.0",
-                  "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz"
-                },
-                "stringstream": {
-                  "version": "0.0.5",
-                  "from": "stringstream@>=0.0.4 <0.1.0",
-                  "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz"
-                },
-                "combined-stream": {
-                  "version": "1.0.5",
-                  "from": "combined-stream@>=1.0.5 <1.1.0",
-                  "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz",
-                  "dependencies": {
-                    "delayed-stream": {
-                      "version": "1.0.0",
-                      "from": "delayed-stream@>=1.0.0 <1.1.0",
-                      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
-                    }
-                  }
-                },
-                "isstream": {
-                  "version": "0.1.2",
-                  "from": "isstream@>=0.1.2 <0.2.0",
-                  "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz"
-                },
-                "is-typedarray": {
-                  "version": "1.0.0",
-                  "from": "is-typedarray@>=1.0.0 <1.1.0",
-                  "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz"
-                },
-                "har-validator": {
-                  "version": "2.0.3",
-                  "from": "har-validator@>=2.0.2 <2.1.0",
-                  "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.3.tgz",
-                  "dependencies": {
-                    "commander": {
-                      "version": "2.9.0",
-                      "from": "commander@>=2.9.0 <3.0.0",
-                      "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz",
-                      "dependencies": {
-                        "graceful-readlink": {
-                          "version": "1.0.1",
-                          "from": "graceful-readlink@>=1.0.0",
-                          "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz"
-                        }
-                      }
-                    },
-                    "is-my-json-valid": {
-                      "version": "2.12.3",
-                      "from": "is-my-json-valid@>=2.12.3 <3.0.0",
-                      "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.12.3.tgz",
-                      "dependencies": {
-                        "generate-function": {
-                          "version": "2.0.0",
-                          "from": "generate-function@>=2.0.0 <3.0.0",
-                          "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz"
-                        },
-                        "generate-object-property": {
-                          "version": "1.2.0",
-                          "from": "generate-object-property@>=1.1.0 <2.0.0",
-                          "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz",
-                          "dependencies": {
-                            "is-property": {
-                              "version": "1.0.2",
-                              "from": "is-property@>=1.0.0 <2.0.0",
-                              "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz"
-                            }
-                          }
-                        },
-                        "jsonpointer": {
-                          "version": "2.0.0",
-                          "from": "jsonpointer@2.0.0",
-                          "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-2.0.0.tgz"
-                        },
-                        "xtend": {
-                          "version": "4.0.1",
-                          "from": "xtend@>=4.0.0 <5.0.0",
-                          "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz"
-                        }
-                      }
-                    },
-                    "pinkie-promise": {
-                      "version": "2.0.0",
-                      "from": "pinkie-promise@>=2.0.0 <3.0.0",
-                      "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.0.tgz",
-                      "dependencies": {
-                        "pinkie": {
-                          "version": "2.0.1",
-                          "from": "pinkie@>=2.0.0 <3.0.0",
-                          "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.1.tgz"
-                        }
-                      }
-                    }
-                  }
-                }
-              }
-            },
-            "source-map": {
-              "version": "0.4.4",
-              "from": "source-map@>=0.4.2 <0.5.0",
-              "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
-              "dependencies": {
-                "amdefine": {
-                  "version": "1.0.0",
-                  "from": "amdefine@>=0.0.4",
-                  "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz"
-                }
-              }
-            }
-          }
-        },
         "lodash": {
           "version": "3.10.1",
           "from": "lodash@>=3.2.0 <4.0.0",
       "from": "grunt-contrib-uglify@0.11.0",
       "resolved": "https://registry.npmjs.org/grunt-contrib-uglify/-/grunt-contrib-uglify-0.11.0.tgz",
       "dependencies": {
-        "chalk": {
-          "version": "1.1.1",
-          "from": "chalk@>=1.0.0 <2.0.0",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.1.tgz",
-          "dependencies": {
-            "ansi-styles": {
-              "version": "2.1.0",
-              "from": "ansi-styles@>=2.1.0 <3.0.0",
-              "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.1.0.tgz"
-            },
-            "escape-string-regexp": {
-              "version": "1.0.3",
-              "from": "escape-string-regexp@>=1.0.2 <2.0.0",
-              "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.3.tgz"
-            },
-            "has-ansi": {
-              "version": "2.0.0",
-              "from": "has-ansi@>=2.0.0 <3.0.0",
-              "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
-              "dependencies": {
-                "ansi-regex": {
-                  "version": "2.0.0",
-                  "from": "ansi-regex@>=2.0.0 <3.0.0",
-                  "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz"
-                }
-              }
-            },
-            "strip-ansi": {
-              "version": "3.0.0",
-              "from": "strip-ansi@>=3.0.0 <4.0.0",
-              "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.0.tgz",
-              "dependencies": {
-                "ansi-regex": {
-                  "version": "2.0.0",
-                  "from": "ansi-regex@>=2.0.0 <3.0.0",
-                  "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz"
-                }
-              }
-            },
-            "supports-color": {
-              "version": "2.0.0",
-              "from": "supports-color@>=2.0.0 <3.0.0",
-              "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz"
-            }
-          }
-        },
         "lodash": {
           "version": "3.10.1",
           "from": "lodash@>=3.2.0 <4.0.0",
           "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz"
+        }
+      }
+    },
+    "grunt-contrib-watch": {
+      "version": "0.6.1",
+      "from": "grunt-contrib-watch@>=0.6.1 <0.7.0",
+      "resolved": "https://registry.npmjs.org/grunt-contrib-watch/-/grunt-contrib-watch-0.6.1.tgz",
+      "dependencies": {
+        "async": {
+          "version": "0.2.10",
+          "from": "async@>=0.2.9 <0.3.0",
+          "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz"
         },
-        "maxmin": {
-          "version": "1.1.0",
-          "from": "maxmin@>=1.0.0 <2.0.0",
-          "resolved": "https://registry.npmjs.org/maxmin/-/maxmin-1.1.0.tgz",
-          "dependencies": {
-            "figures": {
-              "version": "1.4.0",
-              "from": "figures@>=1.0.1 <2.0.0",
-              "resolved": "https://registry.npmjs.org/figures/-/figures-1.4.0.tgz"
-            },
-            "gzip-size": {
-              "version": "1.0.0",
-              "from": "gzip-size@>=1.0.0 <2.0.0",
-              "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-1.0.0.tgz",
-              "dependencies": {
-                "concat-stream": {
-                  "version": "1.5.1",
-                  "from": "concat-stream@>=1.4.1 <2.0.0",
-                  "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.1.tgz",
-                  "dependencies": {
-                    "inherits": {
-                      "version": "2.0.1",
-                      "from": "inherits@>=2.0.1 <2.1.0",
-                      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
-                    },
-                    "typedarray": {
-                      "version": "0.0.6",
-                      "from": "typedarray@>=0.0.5 <0.1.0",
-                      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz"
-                    },
-                    "readable-stream": {
-                      "version": "2.0.4",
-                      "from": "readable-stream@>=2.0.0 <2.1.0",
-                      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.4.tgz",
-                      "dependencies": {
-                        "core-util-is": {
-                          "version": "1.0.2",
-                          "from": "core-util-is@>=1.0.0 <1.1.0",
-                          "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
-                        },
-                        "isarray": {
-                          "version": "0.0.1",
-                          "from": "isarray@0.0.1",
-                          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
-                        },
-                        "process-nextick-args": {
-                          "version": "1.0.6",
-                          "from": "process-nextick-args@>=1.0.0 <1.1.0",
-                          "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.6.tgz"
-                        },
-                        "string_decoder": {
-                          "version": "0.10.31",
-                          "from": "string_decoder@>=0.10.0 <0.11.0",
-                          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
-                        },
-                        "util-deprecate": {
-                          "version": "1.0.2",
-                          "from": "util-deprecate@>=1.0.1 <1.1.0",
-                          "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
-                        }
-                      }
-                    }
-                  }
-                },
-                "browserify-zlib": {
-                  "version": "0.1.4",
-                  "from": "browserify-zlib@>=0.1.4 <0.2.0",
-                  "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz",
-                  "dependencies": {
-                    "pako": {
-                      "version": "0.2.8",
-                      "from": "pako@>=0.2.0 <0.3.0",
-                      "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.8.tgz"
-                    }
-                  }
-                }
-              }
-            },
-            "pretty-bytes": {
-              "version": "1.0.4",
-              "from": "pretty-bytes@>=1.0.0 <2.0.0",
-              "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-1.0.4.tgz",
-              "dependencies": {
-                "get-stdin": {
-                  "version": "4.0.1",
-                  "from": "get-stdin@>=4.0.1 <5.0.0",
-                  "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz"
-                },
-                "meow": {
-                  "version": "3.6.0",
-                  "from": "meow@>=3.1.0 <4.0.0",
-                  "resolved": "https://registry.npmjs.org/meow/-/meow-3.6.0.tgz",
-                  "dependencies": {
-                    "camelcase-keys": {
-                      "version": "2.0.0",
-                      "from": "camelcase-keys@>=2.0.0 <3.0.0",
-                      "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.0.0.tgz",
-                      "dependencies": {
-                        "camelcase": {
-                          "version": "2.0.1",
-                          "from": "camelcase@>=2.0.0 <3.0.0",
-                          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.0.1.tgz"
-                        },
-                        "map-obj": {
-                          "version": "1.0.1",
-                          "from": "map-obj@>=1.0.0 <2.0.0",
-                          "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz"
-                        }
-                      }
-                    },
-                    "loud-rejection": {
-                      "version": "1.2.0",
-                      "from": "loud-rejection@>=1.0.0 <2.0.0",
-                      "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.2.0.tgz",
-                      "dependencies": {
-                        "signal-exit": {
-                          "version": "2.1.2",
-                          "from": "signal-exit@>=2.1.2 <3.0.0",
-                          "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-2.1.2.tgz"
-                        }
-                      }
-                    },
-                    "minimist": {
-                      "version": "1.2.0",
-                      "from": "minimist@>=1.1.3 <2.0.0",
-                      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
-                    },
-                    "normalize-package-data": {
-                      "version": "2.3.5",
-                      "from": "normalize-package-data@>=2.3.4 <3.0.0",
-                      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.3.5.tgz",
-                      "dependencies": {
-                        "hosted-git-info": {
-                          "version": "2.1.4",
-                          "from": "hosted-git-info@>=2.1.4 <3.0.0",
-                          "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.1.4.tgz"
-                        },
-                        "is-builtin-module": {
-                          "version": "1.0.0",
-                          "from": "is-builtin-module@>=1.0.0 <2.0.0",
-                          "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz",
-                          "dependencies": {
-                            "builtin-modules": {
-                              "version": "1.1.0",
-                              "from": "builtin-modules@>=1.0.0 <2.0.0",
-                              "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.0.tgz"
-                            }
-                          }
-                        },
-                        "semver": {
-                          "version": "5.1.0",
-                          "from": "semver@>=2.0.0 <3.0.0||>=3.0.0 <4.0.0||>=4.0.0 <5.0.0||>=5.0.0 <6.0.0",
-                          "resolved": "https://registry.npmjs.org/semver/-/semver-5.1.0.tgz"
-                        },
-                        "validate-npm-package-license": {
-                          "version": "3.0.1",
-                          "from": "validate-npm-package-license@>=3.0.1 <4.0.0",
-                          "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz",
-                          "dependencies": {
-                            "spdx-correct": {
-                              "version": "1.0.2",
-                              "from": "spdx-correct@>=1.0.0 <1.1.0",
-                              "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz",
-                              "dependencies": {
-                                "spdx-license-ids": {
-                                  "version": "1.1.0",
-                                  "from": "spdx-license-ids@>=1.0.2 <2.0.0",
-                                  "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.1.0.tgz"
-                                }
-                              }
-                            },
-                            "spdx-expression-parse": {
-                              "version": "1.0.2",
-                              "from": "spdx-expression-parse@>=1.0.0 <1.1.0",
-                              "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.2.tgz",
-                              "dependencies": {
-                                "spdx-exceptions": {
-                                  "version": "1.0.4",
-                                  "from": "spdx-exceptions@>=1.0.4 <2.0.0",
-                                  "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-1.0.4.tgz"
-                                },
-                                "spdx-license-ids": {
-                                  "version": "1.1.0",
-                                  "from": "spdx-license-ids@>=1.0.2 <2.0.0",
-                                  "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.1.0.tgz"
-                                }
-                              }
-                            }
-                          }
-                        }
-                      }
-                    },
-                    "object-assign": {
-                      "version": "4.0.1",
-                      "from": "object-assign@>=4.0.1 <5.0.0",
-                      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.0.1.tgz"
-                    },
-                    "read-pkg-up": {
-                      "version": "1.0.1",
-                      "from": "read-pkg-up@>=1.0.1 <2.0.0",
-                      "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
-                      "dependencies": {
-                        "find-up": {
-                          "version": "1.1.0",
-                          "from": "find-up@>=1.0.0 <2.0.0",
-                          "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.0.tgz",
-                          "dependencies": {
-                            "path-exists": {
-                              "version": "2.1.0",
-                              "from": "path-exists@>=2.0.0 <3.0.0",
-                              "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz"
-                            },
-                            "pinkie-promise": {
-                              "version": "2.0.0",
-                              "from": "pinkie-promise@>=2.0.0 <3.0.0",
-                              "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.0.tgz",
-                              "dependencies": {
-                                "pinkie": {
-                                  "version": "2.0.1",
-                                  "from": "pinkie@>=2.0.0 <3.0.0",
-                                  "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.1.tgz"
-                                }
-                              }
-                            }
-                          }
-                        },
-                        "read-pkg": {
-                          "version": "1.1.0",
-                          "from": "read-pkg@>=1.0.0 <2.0.0",
-                          "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
-                          "dependencies": {
-                            "load-json-file": {
-                              "version": "1.1.0",
-                              "from": "load-json-file@>=1.0.0 <2.0.0",
-                              "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
-                              "dependencies": {
-                                "graceful-fs": {
-                                  "version": "4.1.2",
-                                  "from": "graceful-fs@>=4.1.2 <5.0.0",
-                                  "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.2.tgz"
-                                },
-                                "parse-json": {
-                                  "version": "2.2.0",
-                                  "from": "parse-json@>=2.2.0 <3.0.0",
-                                  "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
-                                  "dependencies": {
-                                    "error-ex": {
-                                      "version": "1.3.0",
-                                      "from": "error-ex@>=1.2.0 <2.0.0",
-                                      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.0.tgz",
-                                      "dependencies": {
-                                        "is-arrayish": {
-                                          "version": "0.2.1",
-                                          "from": "is-arrayish@>=0.2.1 <0.3.0",
-                    &nbs