Merge branch 'MDL-54559-master' of git://github.com/junpataleta/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Tue, 14 Jun 2016 06:15:27 +0000 (14:15 +0800)
committerDan Poltawski <dan@moodle.com>
Thu, 16 Jun 2016 08:04:08 +0000 (09:04 +0100)
73 files changed:
.eslintrc [new file with mode: 0644]
.gitignore
Gruntfile.js
admin/settings/courses.php
admin/tool/lp/amd/src/competency_rule_points.js
admin/tool/lp/lib.php
admin/tool/lp/pix/competency.png [deleted file]
admin/tool/lp/pix/competency.svg [deleted file]
admin/tool/lp/upgrade.txt [new file with mode: 0644]
admin/user/user_bulk.php
admin/user/user_bulk_enrol.php [deleted file]
admin/user/user_bulk_forms.php
backup/moodle2/restore_qtype_plugin.class.php
blocks/course_overview/tests/behat/quiz_overview.feature [new file with mode: 0644]
blocks/navigation/styles.css
blocks/settings/styles.css
course/externallib.php
course/format/lib.php
course/lib.php
course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-debug.js
course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-min.js
course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes.js
course/yui/src/toolboxes/js/resource.js
install/lang/he/admin.php
install/lang/he/install.php
install/lang/he/moodle.php
install/lang/pt/install.php
lang/en/error.php
lib/amd/src/localstorage.js
lib/amd/src/yui.js
lib/classes/session/memcached.php
lib/editor/atto/autosave-ajax.php
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/src/editor/build.json
lib/editor/atto/yui/src/editor/js/autosave-io.js [new file with mode: 0644]
lib/editor/atto/yui/src/editor/js/autosave.js
lib/editor/atto/yui/src/editor/meta/editor.json
lib/form/tags.php
lib/outputrenderers.php
lib/tests/behat/behat_hooks.php
lib/tests/weblib_format_text_test.php
lib/weblib.php
message/externallib.php
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/annotationstamp.js
mod/assign/feedback/editpdf/yui/src/editor/js/comment.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/mod_form.php
mod/assign/tests/base_test.php
mod/assign/tests/locallib_test.php
mod/data/classes/search/activity.php [new file with mode: 0644]
mod/data/lang/en/data.php
mod/forum/lib.php
mod/lti/locallib.php
mod/quiz/lib.php
mod/quiz/report/overview/report.php
mod/quiz/tests/lib_test.php
mod/wiki/create_form.php
npm-shrinkwrap.json
package.json
pix/i/competencies.png [new file with mode: 0644]
pix/i/competencies.svg [new file with mode: 0644]
portfolio/download/lib.php
tag/classes/output/tagareacollection.php
tag/classes/tag.php
theme/index.php
version.php

diff --git a/.eslintrc b/.eslintrc
new file mode 100644 (file)
index 0000000..31bd8ab
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,164 @@
+{
+  'env': {
+    'browser': true,
+    'amd': true
+  },
+  'globals': {
+    'M': true,
+    'Y': true
+  },
+  'rules': {
+    // See http://eslint.org/docs/rules/ for all rules and explanations of all
+    // rules. Commented out rules with 'DEFINE POLICY' are rules Dan P has flagged
+    // for discussion and possible enable soon.
+    // === Possible Errors ===
+    // DEFINE POLICY: 'comma-dangle': ['off', 'always'],
+    'no-cond-assign': 'error',
+    'no-console': 'error',
+    'no-constant-condition': 'error',
+    'no-control-regex': 'error',
+    'no-debugger': 'error',
+    'no-dupe-args': 'error',
+    'no-dupe-keys': 'error',
+    'no-duplicate-case': 'error',
+    // Disabled for YUI rollups, enabled by grunt for AMD: 'no-empty': 'error',
+    'no-empty-character-class': 'error',
+    'no-ex-assign': 'error',
+    'no-extra-boolean-cast': 'error',
+    'no-extra-parens': 'off',
+    'no-extra-semi': 'error',
+    'no-func-assign': 'error',
+    'no-inner-declarations': 'error',
+    'no-invalid-regexp': 'error',
+    'no-irregular-whitespace': 'error',
+    'no-negated-in-lhs': 'error',
+    'no-obj-calls': 'error',
+    'no-prototype-builtins': 'off',
+    'no-regex-spaces': 'error',
+    'no-sparse-arrays': 'error',
+    'no-unexpected-multiline': 'error',
+    'no-unreachable': 'warn',
+    'no-unsafe-finally': 'error',
+    'use-isnan': 'error',
+    'valid-jsdoc': ['warn', { 'requireReturn': false }],
+    'valid-typeof': 'error',
+
+    // === Best Practices ===
+    // (these mostly match our jshint config)
+    'curly': 'error',
+    'dot-notation': 'warn',
+    'no-alert': 'warn',
+    'no-caller': 'error',
+    'no-case-declarations': 'error',
+    'no-empty-pattern': 'error',
+    'no-empty-function': 'warn',
+    //DEFINE POLICY: 'no-eq-null': 'warn',
+    'no-eval': 'error',
+    //DEFINE POLICY: 'no-extra-bind': 'warn',
+    'no-fallthrough': 'error',
+    //DEFINE POLICY: 'no-implicit-globals': 'warn',
+    'no-implied-eval': 'error',
+    'no-invalid-this': 'error',
+    'no-iterator': 'error',
+    'no-labels': 'error',
+    'no-loop-func': 'error',
+    'no-multi-spaces': 'warn',
+    'no-multi-str': 'error',
+    'no-native-reassign': 'warn',
+    'no-new-func': 'error',
+    'no-new-wrappers': 'error',
+    // DEFINE POLICY: no-octal: "error"
+    // DEFINE POLICY: no-octal-escape: "error"
+    'no-proto': 'error',
+    'no-redeclare': 'warn',
+    'no-return-assign': 'error',
+    'no-script-url': 'error',
+    'no-self-assign': 'error',
+    'no-self-compare': 'error',
+    'no-unmodified-loop-condition': 'error',
+    // Disabled for YUI rollups, enabled by grunt for AMD: 'no-unused-expressions': 'error',
+    'no-unused-labels': 'error',
+    //DEFINE POLICY: 'no-useless-call': 'error',
+    'no-useless-escape': 'warn',
+    //DEFINE POLICY: 'no-with': 'error',
+    'wrap-iife': ['error', 'any'],
+
+    // === Variables ===
+    'no-delete-var': 'error',
+    // Disabled for YUI rollups, enabled by grunt for AMD: 'no-undef': 'off',
+    //DEFINE POLICY: 'no-undef-init': 'error',
+    // Disabled for YUI rollups, enabled by grunt for AMD: 'no-unused-vars': 'error',
+
+    // === Stylistic Issues ===
+    'array-bracket-spacing': 'warn',
+    'block-spacing': 'warn',
+    'brace-style': ['warn', '1tbs'],
+    'camelcase': 'warn',
+    'comma-spacing': ['warn', { 'before': false, 'after': true }],
+    'comma-style': ['warn', 'last'],
+    'computed-property-spacing': 'error',
+    'consistent-this': 'off',
+    'eol-last': 'off',
+    'func-names': 'off',
+    'func-style': 'off',
+    // indent currently not doing well with our wrapping style, so disabled.
+    'indent': ['off', 4, { 'SwitchCase': 1 }],
+    'key-spacing': ['warn', { 'beforeColon': false, 'afterColon': true, 'mode': minimum }],
+    'keyword-spacing': 'warn',
+    'linebreak-style': ['error', 'unix'],
+    'lines-around-comment': 'off',
+    'max-len': ['error', 132],
+    'max-lines': 'off',
+    // DEFINE POLICY: turn on some of these max values?
+    'max-depth': 'off',
+    'max-nested-callbacks': 'off',
+    'max-params': 'off',
+    'max-statements': 'off',
+    'max-statements-per-line': 'off',
+    'new-cap': 'warn',
+    'new-parens': 'warn',
+    'newline-after-var': 'off',
+    'newline-before-return': 'off',
+    // REVIST POLICY: 'newline-per-chained-call': 'warn',
+    'no-array-constructor': 'off',
+    'no-bitwise': 'error',
+    'no-continue': 'off',
+    'no-inline-comments': 'off',
+    'no-lonely-if': 'off',
+    'no-mixed-operators': 'off',
+    'no-mixed-spaces-and-tabs': 'error',
+    'no-multiple-empty-lines': 'warn',
+    'no-negated-condition': 'off',
+    'no-nested-ternary': 'warn',
+    'no-new-object': 'off',
+    'no-plusplus': 'off',
+    'no-spaced-func': 'warn',
+    'no-ternary': 'off',
+    'no-trailing-spaces': 'error',
+    'no-underscore-dangle': 'off',
+    // DEFINE POLICY: 'no-unneeded-ternary': 'off',
+    'no-whitespace-before-property': 'warn',
+    // DEFINE POLICY: 'object-curly-newline': 'off,
+    // DEFINE POLICY: 'object-curly-spacing': 'off',
+    // DEFINE POLICY: 'object-property-newline': 'off',
+    'one-var': 'off',
+    // DEFINE POLICY: 'one-var-declaration-per-line': 'off',
+    'operator-assignment': 'off',
+    'operator-linebreak': 'off',
+    'padded-blocks': 'off',
+    // DEFINE POLICY: 'quote-props': 'off',
+    'quotes': 'off',
+    'require-jsdoc': 'warn',
+    'semi': 'error',
+    'semi-spacing': ['warn', {'before': false, 'after': true}],
+    'sort-vars': 'off',
+    'space-before-blocks': 'warn',
+    'space-before-function-paren': ['warn', 'never'],
+    'space-in-parens': 'warn',
+    'space-infix-ops': 'warn',
+    'space-unary-ops': 'warn',
+    'spaced-comment': 'warn',
+    'unicode-bom': 'error',
+    'wrap-regex': 'off',
+  }
+}
index 1dab0f7..71a7224 100644 (file)
@@ -36,3 +36,4 @@ composer.phar
 /lib/yuilib/*/*/*-coverage.js
 atlassian-ide-plugin.xml
 /node_modules/
+.eslintignore
index 7412970..a1d525e 100644 (file)
@@ -13,6 +13,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 */
+/* eslint-env node */
 
 /**
  * @copyright  2014 Andrew Nicols
 module.exports = function(grunt) {
     var path = require('path'),
         tasks = {},
-        cwd = process.env.PWD || process.cwd();
+        cwd = process.env.PWD || process.cwd(),
+        async = require('async'),
+        DOMParser = require('xmldom').DOMParser,
+        xpath = require('xpath');
 
     // Windows users can't run grunt in a subdirectory, so allow them to set
     // the root by passing --root=path/to/dir.
@@ -34,9 +38,9 @@ module.exports = function(grunt) {
         var root = grunt.option('root');
         if (grunt.file.exists(__dirname, root)) {
             cwd = path.join(__dirname, root);
-            grunt.log.ok('Setting root to '+cwd);
+            grunt.log.ok('Setting root to ' + cwd);
         } else {
-            grunt.fail.fatal('Setting root to '+root+' failed - path does not exist');
+            grunt.fail.fatal('Setting root to ' + root + ' failed - path does not exist');
         }
     }
 
@@ -55,25 +59,93 @@ module.exports = function(grunt) {
      * @param {String} srcPath the  matched src path
      * @return {String} The rewritten destination path.
      */
-    var uglify_rename = function (destPath, srcPath) {
+    var uglifyRename = function(destPath, srcPath) {
         destPath = srcPath.replace('src', 'build');
         destPath = destPath.replace('.js', '.min.js');
         destPath = path.resolve(cwd, destPath);
         return destPath;
     };
 
+    /**
+     * Find thirdpartylibs.xml and generate an array of paths contained within
+     * them (used to generate ignore files and so on).
+     *
+     * @return {array} The list of thirdparty paths.
+     */
+    var getThirdPartyPathsFromXML = function() {
+        var thirdpartyfiles = grunt.file.expand('*/**/thirdpartylibs.xml');
+        var libs = ['node_modules/', 'vendor/'];
+
+        thirdpartyfiles.forEach(function(file) {
+          var dirname = path.dirname(file);
+
+          var doc = new DOMParser().parseFromString(grunt.file.read(file));
+          var nodes = xpath.select("/libraries/library/location/text()", doc);
+
+          nodes.forEach(function(node) {
+            var lib = path.join(dirname, node.toString());
+            if (grunt.file.isDir(lib)) {
+                // Ensure trailing slash on dirs.
+                lib = lib.replace(/\/?$/, '/');
+            }
+
+            // Look for duplicate paths before adding to array.
+            if (libs.indexOf(lib) === -1) {
+                libs.push(lib);
+            }
+          });
+        });
+        return libs;
+    };
+
+    // An array of paths to third party directories.
+    var thirdPartyPaths = getThirdPartyPathsFromXML();
+
+    /**
+     * Determine if the file is a Moodle file, or its listed in
+     * the thirdpartylibs.xml file paths as a third party file.
+     *
+     * @param {string} file The file path to determine if thirdparty
+     * @return {bool} false If thid party file.
+     */
+    var isMoodleFile = function(file) {
+      if (grunt.file.isMatch(thirdPartyPaths, file)) {
+        return false;
+      }
+      return true;
+    };
+
     // Project configuration.
     grunt.initConfig({
         jshint: {
             options: {jshintrc: '.jshintrc'},
             amd: { src: amdSrc }
         },
+        eslint: {
+            // Even though warnings dont stop the build we don't display warnings by default because
+            // at this moment we've got too many core warnings.
+            options: { quiet: !grunt.option('show-lint-warnings') },
+            // Check AMD files. We add some stricter rules which we can't apply to the default configuration due
+            // to YUI rollups.
+            amd: {
+              src: amdSrc,
+              filter: isMoodleFile,
+              options: {
+                  rules: {'no-undef': 'error', 'no-unused-vars': 'error', 'no-empty': 'error', 'no-unused-expressions': 'error'}
+              }
+            },
+            // Check YUI module source files.
+            yui: {
+               src: ['**/yui/src/**/*.js', '!*/**/yui/src/*/meta/*.js'],
+               filter: isMoodleFile
+            }
+        },
         uglify: {
             amd: {
                 files: [{
                     expand: true,
                     src: amdSrc,
-                    rename: uglify_rename
+                    rename: uglifyRename
                 }]
             }
         },
@@ -102,7 +174,7 @@ module.exports = function(grunt) {
             },
             yui: {
                 files: ['**/yui/src/**/*.js'],
-                tasks: ['shifter']
+                tasks: ['yui']
             },
         },
         shifter: {
@@ -113,6 +185,15 @@ module.exports = function(grunt) {
         }
     });
 
+    /**
+     * Generate ignore files (utilising thirdpartylibs.xml data)
+     */
+    tasks.ignorefiles = function() {
+      // Generate .eslintignore.
+      var eslintIgnores = ['# Generated by "grunt ignorefiles"', '*/**/yui/src/*/meta/', '*/**/build/'].concat(thirdPartyPaths);
+      grunt.file.write('.eslintignore', eslintIgnores.join('\n'));
+    };
+
     /**
      * 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.
@@ -121,14 +202,13 @@ module.exports = function(grunt) {
      * so be careful to to call done().
      */
     tasks.shifter = function() {
-        var async = require('async'),
-            done = this.async(),
+        var done = this.async(),
             options = grunt.config('shifter.options');
 
         // Run the shifter processes one at a time to avoid confusing output.
-        async.eachSeries(options.paths, function (src, filedone) {
+        async.eachSeries(options.paths, function(src, filedone) {
             var args = [];
-            args.push( path.normalize(__dirname + '/node_modules/shifter/bin/shifter'));
+            args.push(path.normalize(__dirname + '/node_modules/shifter/bin/shifter'));
 
             // Always ignore the node_modules directory.
             args.push('--excludes', 'node_modules');
@@ -176,7 +256,7 @@ module.exports = function(grunt) {
                     cmd: "node",
                     args: args,
                     opts: {cwd: src, stdio: 'inherit', env: process.env}
-                }, function (error, result, code) {
+                }, function(error, result, code) {
                     if (code) {
                         grunt.fail.fatal('Shifter failed with code: ' + code);
                     } else {
@@ -205,7 +285,7 @@ module.exports = function(grunt) {
     tasks.startup = function() {
         // Are we in a YUI directory?
         if (path.basename(path.resolve(cwd, '../../')) == 'yui') {
-            grunt.task.run('shifter');
+            grunt.task.run('yui');
         // Are we in an AMD directory?
         } else if (inAMD) {
             grunt.task.run('amd');
@@ -223,7 +303,7 @@ module.exports = function(grunt) {
     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('uglify.amd.files', [{ expand: true, src: files, rename: uglifyRename }]);
           grunt.config('shifter.options.paths', files);
           changedFiles = Object.create(null);
     }, 200);
@@ -238,11 +318,14 @@ module.exports = function(grunt) {
     grunt.loadNpmTasks('grunt-contrib-jshint');
     grunt.loadNpmTasks('grunt-contrib-less');
     grunt.loadNpmTasks('grunt-contrib-watch');
+    grunt.loadNpmTasks('grunt-eslint');
 
     // Register JS tasks.
     grunt.registerTask('shifter', 'Run Shifter against the current directory', tasks.shifter);
-    grunt.registerTask('amd', ['jshint', 'uglify']);
-    grunt.registerTask('js', ['amd', 'shifter']);
+    grunt.registerTask('ignorefiles', 'Generate ignore files for linters', tasks.ignorefiles);
+    grunt.registerTask('yui', ['eslint:yui', 'shifter']);
+    grunt.registerTask('amd', ['eslint:amd', 'jshint', 'uglify']);
+    grunt.registerTask('js', ['amd', 'yui']);
 
     // Register CSS taks.
     grunt.registerTask('css', ['less:bootstrapbase']);
index 21ecab7..2d0dd1b 100644 (file)
@@ -72,13 +72,13 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
         $formcourseformats[$courseformat] = new lang_string('pluginname', "format_$courseformat");
     }
     $temp->add(new admin_setting_configselect('moodlecourse/format', new lang_string('format'), new lang_string('coursehelpformat'),
-        'weeks',$formcourseformats));
+        'topics', $formcourseformats));
 
     $temp->add(new admin_setting_configtext('moodlecourse/maxsections', new lang_string('maxnumberweeks'),
         new lang_string('maxnumberweeks_desc'), 52));
 
     $temp->add(new admin_settings_num_course_sections('moodlecourse/numsections', new lang_string('numberweeks'),
-        new lang_string('coursehelpnumberweeks'), 10));
+        new lang_string('coursehelpnumberweeks'), 4));
 
     $choices = array();
     $choices['0'] = new lang_string('hiddensectionscollapsed');
index 470bb5d..db4bfe4 100644 (file)
@@ -126,6 +126,7 @@ define(['jquery',
             try {
                 config = JSON.parse(self._competency.ruleconfig);
             } catch (e) {
+                // eslint-disable-line no-empty
             }
         }
 
index b2e894d..870104e 100644 (file)
@@ -44,7 +44,7 @@ function tool_lp_extend_navigation_course($navigation, $course, $coursecontext)
                                             navigation_node::TYPE_SETTING,
                                             null,
                                             null,
-                                            new pix_icon('competency', '', 'tool_lp'));
+                                            new pix_icon('i/competencies', ''));
     if (isset($settingsnode)) {
         $navigation->add_node($settingsnode);
     }
@@ -129,7 +129,7 @@ function tool_lp_extend_navigation_category_settings($navigation, $coursecategor
                                                 navigation_node::TYPE_SETTING,
                                                 null,
                                                 null,
-                                                new pix_icon('competency', '', 'tool_lp'));
+                                                new pix_icon('i/competencies', ''));
         if (isset($settingsnode)) {
             $navigation->add_node($settingsnode);
         }
@@ -144,7 +144,7 @@ function tool_lp_extend_navigation_category_settings($navigation, $coursecategor
                                                 navigation_node::TYPE_SETTING,
                                                 null,
                                                 null,
-                                                new pix_icon('competency', '', 'tool_lp'));
+                                                new pix_icon('i/competencies', ''));
         if (isset($settingsnode)) {
             $navigation->add_node($settingsnode);
         }
diff --git a/admin/tool/lp/pix/competency.png b/admin/tool/lp/pix/competency.png
deleted file mode 100644 (file)
index 52fe956..0000000
Binary files a/admin/tool/lp/pix/competency.png and /dev/null differ
diff --git a/admin/tool/lp/pix/competency.svg b/admin/tool/lp/pix/competency.svg
deleted file mode 100644 (file)
index e5f670e..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   width="32"
-   height="32"
-   id="svg2"
-   version="1.1"
-   inkscape:version="0.48.4 r9939"
-   sodipodi:docname="New document 1" preserveAspectRatio="xMinYMid meet">
-  <defs
-     id="defs4" />
-  <sodipodi:namedview
-     id="base"
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1.0"
-     inkscape:pageopacity="0.0"
-     inkscape:pageshadow="2"
-     inkscape:zoom="5.6"
-     inkscape:cx="32.609372"
-     inkscape:cy="35.818362"
-     inkscape:document-units="px"
-     inkscape:current-layer="layer1"
-     showgrid="false"
-     width="33px"
-     inkscape:window-width="1916"
-     inkscape:window-height="981"
-     inkscape:window-x="0"
-     inkscape:window-y="72"
-     inkscape:window-maximized="0" />
-  <metadata
-     id="metadata7">
-    <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title></dc:title>
-      </cc:Work>
-    </rdf:RDF>
-  </metadata>
-  <g
-     inkscape:label="Layer 1"
-     inkscape:groupmode="layer"
-     id="layer1"
-     transform="translate(0,-1020.3622)">
-    <g
-       id="g3050"
-       transform="matrix(0.92121849,0,0,0.92121849,-35.507391,91.120599)">
-      <path
-         style="fill:#fafafa"
-         d="m 38.892857,1026.2908 0,-17 17,0 17,0 0,17 0,17 -17,0 -17,0 0,-17 z"
-         id="path3056"
-         inkscape:connector-curvature="0" />
-      <path
-         style="fill:#989898"
-         d="m 40.954049,1037.3439 c -3.465234,-3.0054 -1.687876,-4.0531 6.875853,-4.0531 6.919599,0 7.985143,0.2502 9.263277,2.1752 0.947236,1.4266 2.021632,1.9536 3.121972,1.5314 1.37559,-0.5279 1.677706,-2.0754 1.677706,-8.5934 0,-4.3723 0.273145,-8.6614 0.606989,-9.5314 0.509481,-1.3277 -0.303204,-1.5818 -5.059017,-1.5818 -3.522693,0 -5.432219,-0.3783 -5.047972,-1 1.028329,-1.6639 14.737848,-1.2221 17.277494,0.5567 3.593622,2.5171 2.823056,4.4433 -1.777494,4.4433 l -4,0 0,7.345 c 0,9.8743 -0.892111,10.655 -12.175731,10.655 -7.005509,0 -8.916904,-0.3458 -10.763077,-1.9469 z m 3.938808,-10.6589 c 0,-2.9538 0.324365,-3.3942 2.5,-3.3942 2.153991,0 2.499902,0.4498 2.499294,3.25 -6e-4,2.7611 -0.376693,3.2717 -2.5,3.3942 -2.203157,0.1271 -2.499294,-0.2579 -2.499294,-3.25 z m 7.428571,-1.3942 c 0,-1.6931 0.666667,-2 4.344732,-2 5.000845,0 4.927087,1.9191 -0.08232,2.1419 -3.011559,0.1339 -2.966069,0.1822 0.809017,0.8581 l 4,0.7162 -4.535714,0.1419 c -3.91635,0.1225 -4.535715,-0.1312 -4.535715,-1.8581 z m -6.761904,-3.6667 c -1.167788,-1.1678 -0.718106,-4.0457 0.833333,-5.3333 1.846181,-1.5322 4.5,0.2461 4.5,3.0155 0,2.2918 -3.735778,3.9154 -5.333333,2.3178 z m 6.833333,-0.3333 c 0.339919,-0.55 2.139919,-1 4,-1 1.860081,0 3.660081,0.45 4,1 0.377944,0.6115 -1.175955,1 -4,1 -2.824045,0 -4.377944,-0.3885 -4,-1 z"
-         id="path3054"
-         inkscape:connector-curvature="0" />
-      <path
-         style="fill:#5e5e5e"
-         d="m 40.892857,1036.2908 c -1.027328,-1.9196 -0.76049,-2 6.636076,-2 5.863885,0 8.157677,0.4084 9.593719,1.708 1.429812,1.2939 2.297278,1.4506 3.578741,0.6463 1.318167,-0.8273 1.801811,-3.0713 2.191464,-10.1678 l 0.5,-9.1062 -5.5,-0.351 c -4.40263,-0.2811 -3.893849,-0.3889 2.55,-0.5402 5.16365,-0.1213 8.480263,0.2412 9.25,1.0109 2.011735,2.0117 1.364232,2.8 -2.3,2.8 l -3.5,0 0,7.0657 c 0,10.7479 -0.204518,10.9343 -12,10.9343 -8.906177,0 -10.039956,-0.2061 -11,-2 z m 5.078947,-9.0357 c -0.06465,-2.8954 0.283272,-3.5357 1.921053,-3.5357 1.608782,0 2,0.6666 2,3.4081 0,2.8111 -0.245749,3.2042 -1.402754,2.244 -1.077175,-0.894 -1.523051,-0.8644 -1.921053,0.1276 -0.285064,0.7104 -0.553825,-0.2994 -0.597246,-2.244 z m 8.682712,-0.6716 c 1.243913,-0.2392 3.043913,-0.2301 4,0.02 0.956088,0.2503 -0.06166,0.446 -2.261659,0.4349 -2.2,-0.011 -2.982253,-0.2159 -1.738341,-0.4551 z m 0,-3 c 1.243913,-0.2392 3.043913,-0.2301 4,0.02 0.956088,0.2503 -0.06166,0.446 -2.261659,0.4349 -2.2,-0.011 -2.982253,-0.2159 -1.738341,-0.4551 z m 0,-2 c 1.243913,-0.2392 3.043913,-0.2301 4,0.02 0.956088,0.2503 -0.06166,0.446 -2.261659,0.4349 -2.2,-0.011 -2.982253,-0.2159 -1.738341,-0.4551 z m -9.205886,-1.2025 c -0.810187,-1.3109 0.447721,-4.0902 1.851216,-4.0902 1.828446,0 3.499178,2.081 2.907877,3.6219 -0.592966,1.5452 -3.892838,1.87 -4.759093,0.4683 z"
-         id="path3052"
-         inkscape:connector-curvature="0" />
-    </g>
-  </g>
-</svg>
diff --git a/admin/tool/lp/upgrade.txt b/admin/tool/lp/upgrade.txt
new file mode 100644 (file)
index 0000000..4e41971
--- /dev/null
@@ -0,0 +1,10 @@
+This files describes changes in /admin/tool/lp/* - plugins,
+information provided here is intended especially for developers.
+
+=== 3.2 ===
+
+* The icon 'competency.png/svg' has been removed, please use i/competencies instead.
+
+=== 3.1.1 ===
+
+* The plugin icon 'competency.png/svg' will be removed in the future use i/competencies instead.
index 62a4d29..751a75b 100644 (file)
@@ -24,7 +24,6 @@ if ($data = $action_form->get_data()) {
         case 3: redirect($CFG->wwwroot.'/'.$CFG->admin.'/user/user_bulk_delete.php');
         case 4: redirect($CFG->wwwroot.'/'.$CFG->admin.'/user/user_bulk_display.php');
         case 5: redirect($CFG->wwwroot.'/'.$CFG->admin.'/user/user_bulk_download.php');
-        //case 6: redirect($CFG->wwwroot.'/'.$CFG->admin.'/user/user_bulk_enrol.php'); //TODO: MDL-24064
         case 7: redirect($CFG->wwwroot.'/'.$CFG->admin.'/user/user_bulk_forcepasswordchange.php');
         case 8: redirect($CFG->wwwroot.'/'.$CFG->admin.'/user/user_bulk_cohortadd.php');
     }
diff --git a/admin/user/user_bulk_enrol.php b/admin/user/user_bulk_enrol.php
deleted file mode 100644 (file)
index 21ad105..0000000
+++ /dev/null
@@ -1,131 +0,0 @@
-<?php
-/**
-* script for bulk user multi enrol operations
-*/
-
-die('this needs to be rewritten to use new enrol framework, sorry');  //TODO: MDL-24064
-
-require_once('../../config.php');
-require_once($CFG->libdir.'/adminlib.php');
-$processed = optional_param('processed', '', PARAM_BOOL);
-$sort = optional_param('sort', 'fullname', PARAM_ALPHA); //Sort by full name
-$dir  = optional_param('dir', 'asc', PARAM_ALPHA);       //Order to sort (ASC)
-
-require_login();
-admin_externalpage_setup('userbulk');
-require_capability('moodle/role:assign', context_system::instance()); //TODO: use some enrol cap
-$return = $CFG->wwwroot.'/'.$CFG->admin.'/user/user_bulk.php';
-//If no users selected then return to user_bulk.php
-if (empty($SESSION->bulk_users)) {
-    redirect($return);
-}
-$users = $SESSION->bulk_users; //Get users to display
-$usertotal = get_users(false); //Total number of users registered
-$usercount = count($users);    //number of users
-
-echo $OUTPUT->header();
-
-//take user info
-foreach ($users as $key => $id) {
-    $user = $DB->get_record('user', array('id'=>$id));
-    $user->fullname = fullname($user, true);
-    unset($user->firstname);
-    unset($user->lastname);
-    $users[$key] = $user;
-}
-
-// Need to sort by date
-function sort_compare($a, $b) {
-    global $sort, $dir;
-    if($sort == 'lastaccess') {
-        $rez = $b->lastaccess - $a->lastaccess;
-    } else {
-        $rez = strcasecmp(@$a->$sort, @$b->$sort);
-    }
-    return $dir == 'desc' ? -$rez : $rez;
-}
-usort($users, 'sort_compare');
-
-//Take courses data (id, shortname, and fullname)
-$courses = get_courses_page(1, 'c.sortorder ASC', 'c.id,c.shortname,c.fullname,c.visible', $totalcount);
-$table = new html_table();
-$table->width = "95%";
-$columns = array('fullname');
-foreach ($courses as $v)
-{
-    $columns[] = $v->shortname;
-}
-
-//Print columns headers from table
-foreach ($columns as $column) {
-    $strtitle = $column;
-    if ($sort != $column) {
-        $columnicon = '';
-        $columndir = 'asc';
-    } else {
-        $columndir = ($dir == 'asc') ? 'desc' : 'asc';
-        $columnicon = ' <img src="'.$OUTPUT->pix_url('t/'.($dir == 'asc' ? 'down' : 'up' )).'" alt="" />';
-    }
-    $table->head[] = '<a href="user_bulk_enrol.php?sort='.$column.'&amp;dir='.$columndir.'">'.$strtitle.'</a>'.$columnicon;
-    $table->align[] = 'left';
-}
-
-// process data submitting
-if(!empty($processed)) {
-    //Process data form here
-    $total = count($courses) * count($users);
-
-    for ( $i = 0; $i < $total; $i++ )
-    {
-        $param = "selected".$i;
-        $info = optional_param($param, '', PARAM_SEQUENCE);
-        /**
-         * user id:    ids[0]
-         * course id:  ids[1]
-         * enrol stat: ids[2]
-         */
-        $ids = explode(',', $info);
-        if(!empty($ids[2])) {
-            $context = context_course::instance($ids[1]);
-            role_assign(5, $ids[0], $context->id); //TODO: horrible!!
-        } else {
-            if( empty($ids[1] ) ) {
-                continue;
-            }
-            $context = context_course::instance($ids[1]);
-            role_unassign(5, $ids[0], $context->id);
-        }
-    }
-    redirect($return, get_string('changessaved')); //TODO: horrible!!
-}
-
-//Form beginning
-echo '<form id="multienrol" name="multienrol" method="post" action="user_bulk_enrol.php">';
-echo '<input type="hidden" name="processed" value="yes" />';
-$count = 0;
-foreach($users as $user) {
-    $temparray = array (
-        '<a href="'.$CFG->wwwroot.'/user/view.php?id='.$user->id.'&amp;course='.SITEID.'">'.$user->fullname.'</a>'
-    );
-    $mycourses = enrol_get_users_courses($user->id, false);
-    foreach($courses as $acourse) {
-        $state = '';
-        if (isset($mycourses[$acourse->id])) {
-            $state = 'checked="checked"';
-        }
-        $temparray[] = '<input type="hidden" name="selected' . $count .
-                '" value="' . $user->id . ',' . $acourse->id . ',0" />' .
-                '<input type="checkbox" name="selected' . $count .
-                '" value="' . $user->id . ',' . $acourse->id . ',1" ' . $state . '/>';
-        $count++;
-    }
-    $table->data[] = $temparray;
-}
-echo $OUTPUT->heading("$usercount / $usertotal ".get_string('users'));
-echo html_writer::table($table);
-echo '<div class="continuebutton">';
-echo '<input type="submit" name="multienrolsubmit" value="save changes" />';
-echo '</div>';
-echo '</form>';
-
-echo $OUTPUT->footer();
index 0f7c25c..dd410f4 100644 (file)
@@ -24,10 +24,6 @@ class user_bulk_action_form extends moodleform {
         if (has_capability('moodle/user:update', $syscontext)) {
             $actions[5] = get_string('download', 'admin');
         }
-        if (has_capability('moodle/role:assign', $syscontext)){
-             //TODO: MDL-24064
-            //$actions[6] = get_string('enrolmultipleusers', 'admin');
-        }
         if (has_capability('moodle/user:update', $syscontext)) {
             $actions[7] = get_string('forcepasswordchange');
         }
index 60f599a..7520aa3 100644 (file)
@@ -181,7 +181,7 @@ abstract class restore_qtype_plugin extends restore_plugin {
                 $info = new stdClass();
                 $info->filequestionid = $oldquestionid;
                 $info->dbquestionid   = $newquestionid;
-                $info->answer         = $data->answertext;
+                $info->answer         = s($data->answertext);
                 throw new restore_step_exception('error_question_answers_missing_in_db', $info);
             }
             $newitemid = $this->questionanswercache[$data->answertext];
diff --git a/blocks/course_overview/tests/behat/quiz_overview.feature b/blocks/course_overview/tests/behat/quiz_overview.feature
new file mode 100644 (file)
index 0000000..238591f
--- /dev/null
@@ -0,0 +1,94 @@
+@block @block_course_overview @mod_quiz
+Feature: View the quiz being due
+  In order to know what quizzes are due
+  As a student
+  I can visit my dashboard
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | student1 | Student | 1 | student1@example.com |
+      | student2 | Student | 2 | student2@example.com |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1        |
+      | Course 2 | C2        |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | student1 | C1     | student        |
+      | student2 | C2     | student        |
+      | teacher1 | C1     | editingteacher |
+      | teacher1 | C2     | editingteacher |
+    And the following "activities" exist:
+      | activity | course | idnumber | name                    | timeclose  |
+      | quiz     | C1     | Q1A      | Quiz 1A No deadline     | 0          |
+      | quiz     | C1     | Q1B      | Quiz 1B Past deadline   | 1337       |
+      | quiz     | C1     | Q1C      | Quiz 1C Future deadline | 9000000000 |
+      | quiz     | C1     | Q1D      | Quiz 1D Future deadline | 9000000000 |
+      | quiz     | C1     | Q1E      | Quiz 1E Future deadline | 9000000000 |
+      | quiz     | C2     | Q2A      | Quiz 2A Future deadline | 9000000000 |
+    And the following "question categories" exist:
+      | contextlevel | reference | name           |
+      | Course       | C1        | Test questions |
+    And the following "questions" exist:
+      | qtype     | name           | questiontext              | questioncategory |
+      | truefalse | First question | Answer the first question | Test questions   |
+    And quiz "Quiz 1A No deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 1B Past deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 1C Future deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 1D Future deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 1E Future deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 2A Future deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+
+  Scenario: View my quizzes that are due
+    Given I log in as "student1"
+    When I am on homepage
+    Then I should see "You have quizzes that are due" in the "Course overview" "block"
+    And I should see "Quiz 1C Future deadline" in the "Course overview" "block"
+    And I should see "Quiz 1D Future deadline" in the "Course overview" "block"
+    And I should see "Quiz 1E Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1A No deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1B Past deadline" in the "Course overview" "block"
+    And I should not see "Quiz 2A Future deadline" in the "Course overview" "block"
+    And I log out
+    And I log in as "student2"
+    And I should see "You have quizzes that are due" in the "Course overview" "block"
+    And I should not see "Quiz 1C Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1D Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1E Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1A No deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1B Past deadline" in the "Course overview" "block"
+    And I should see "Quiz 2A Future deadline" in the "Course overview" "block"
+
+  Scenario: View my quizzes that are due and never finished
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Quiz 1D Future deadline"
+    And I press "Attempt quiz now"
+    And I follow "Finish attempt ..."
+    And I press "Submit all and finish"
+    And I follow "Course 1"
+    And I follow "Quiz 1E Future deadline"
+    And I press "Attempt quiz now"
+    When I am on homepage
+    Then I should see "You have quizzes that are due" in the "Course overview" "block"
+    And I should see "Quiz 1C Future deadline" in the "Course overview" "block"
+    And I should see "Quiz 1E Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1A No deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1B Past deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1D Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 2A Future deadline" in the "Course overview" "block"
+
index 47ef1c2..140ed42 100644 (file)
@@ -3,6 +3,7 @@
 .block_navigation .block_tree ul {margin-left: 18px;}
 .block_navigation .block_tree p.hasicon {text-indent: -21px; padding-left: 21px;}
 .block_navigation .block_tree p.hasicon img {width: 16px; height: 16px; margin-top: 3px; margin-right: 5px; vertical-align: top;}
+.block_navigation .block_tree p.hasicon.visibleifjs {display: block;}
 
 .block_navigation .block_tree .tree_item {cursor:pointer; padding-left: 0;margin:3px 0px; background-position: 0 50%; background-repeat: no-repeat;}
 .block_navigation .block_tree .tree_item.branch {padding-left: 21px;}
index 31b27ec..e9c0e10 100644 (file)
@@ -1,6 +1,7 @@
 .block_settings .block_tree ul {margin-left: 18px;}
 .block_settings .block_tree p.hasicon {text-indent: -21px; padding-left: 21px;}
 .block_settings .block_tree p.hasicon img {width: 16px; height: 16px; margin-top: 3px; margin-right: 5px; vertical-align: top;}
+.block_settings .block_tree p.hasicon.visibleifjs {display: block;}
 
 .block_settings .block_tree .tree_item.branch {padding-left: 21px;}
 .block_settings .block_tree .tree_item {cursor:pointer; margin:3px 0px; background-position: 0 50%; background-repeat: no-repeat;}
index ea687c6..b348aa1 100644 (file)
@@ -2140,7 +2140,7 @@ class core_course_external extends external_api {
                 'perpage'       => new external_value(PARAM_INT, 'items per page', VALUE_DEFAULT, 0),
                 'requiredcapabilities' => new external_multiple_structure(
                     new external_value(PARAM_CAPABILITY, 'Capability string used to filter courses by permission'),
-                    VALUE_OPTIONAL
+                    'Optional list of required capabilities (used to filter the list)', VALUE_DEFAULT, array()
                 ),
                 'limittoenrolled' => new external_value(PARAM_BOOL, 'limit to enrolled courses', VALUE_DEFAULT, 0),
             )
index 208c188..20f39a4 100644 (file)
@@ -984,14 +984,14 @@ abstract class format_base {
         }
         if (!is_object($section)) {
             $section = $DB->get_record('course_sections', array('course' => $this->get_courseid(), 'section' => $section),
-                'id,section,sequence');
+                'id,section,sequence,summary');
         }
         if (!$section || !$section->section) {
             // Not possible to delete 0-section.
             return false;
         }
 
-        if (!$forcedeleteifnotempty && !empty($section->sequence)) {
+        if (!$forcedeleteifnotempty && (!empty($section->sequence) || !empty($section->summary))) {
             return false;
         }
 
index dddbf45..994b37e 100644 (file)
@@ -1772,6 +1772,7 @@ function course_delete_module($cmid) {
     // very quick on an empty table).
     $DB->delete_records('course_modules_completion', array('coursemoduleid' => $cm->id));
     $DB->delete_records('course_completion_criteria', array('moduleinstance' => $cm->id,
+                                                            'course' => $cm->course,
                                                             'criteriatype' => COMPLETION_CRITERIA_TYPE_ACTIVITY));
 
     // Delete all tag instances associated with the instance of this module.
index 75ef3b8..212e98d 100644 (file)
Binary files a/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-debug.js and b/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-debug.js differ
index 290b316..9a0d330 100644 (file)
Binary files a/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-min.js and b/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes-min.js differ
index 75ef3b8..212e98d 100644 (file)
Binary files a/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes.js and b/course/yui/build/moodle-course-toolboxes/moodle-course-toolboxes.js differ
index 3bb4e60..f475b89 100644 (file)
@@ -128,7 +128,6 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
                 break;
             case 'move':
             case 'update':
-            case 'duplicate':
             case 'assignroles':
                 break;
             default:
index c9f613d..5374ba9 100644 (file)
@@ -42,4 +42,4 @@ $string['cliunknowoption'] = 'אפשרויות לא מוכרות :
 אנא השתמש באפשרות העזרה.';
 $string['cliyesnoprompt'] = 'רשום y (שפרושו כן) או n (שפרושו לא)';
 $string['environmentrequireinstall'] = 'נדרש להתקין/לאפשר זאת';
-$string['environmentrequireversion'] = '×\92×\99רסת {$a->needed} × ×\93רשת ×\95×\90ת×\94 ×\9eר×\99×¥ {$a->current}';
+$string['environmentrequireversion'] = '×\92×\99רסת {$a->needed} × ×\93רשת ×\90×\9a ×\94×\92×\99רס×\94 ×\94× ×\95×\9b×\97×\99ת ×\94×\99×\90 {$a->current}';
index 8dfd3a2..f58e8a3 100644 (file)
@@ -86,10 +86,10 @@ $string['pathsunsecuredataroot'] = 'ספריית המידע (Data Directory) ל
 $string['pathswrongadmindir'] = 'ספריית ה-admin לא קיימת';
 $string['phpextension'] = 'הרחבת PHP {$a}';
 $string['phpversion'] = 'גירסת PHP';
-$string['phpversionhelp'] = '<p>גרסת PHP חייבת להיות לפחות 4.3.0 או 5.1.0 (בגרסאות 5.0.x קיימות מספר בעיות ידועות) </p>
+$string['phpversionhelp'] = '<p>גרסת PHP חייבת להיות לפחות 4.3.0 או 5.1.0 (בגרסאות 5.0 קיימות מספר בעיות ידועות) </p>
 <p> במערכת שלך פועלת כרגע גרסת {$a} </p>
 <p> אתה חייב לשדרג את גרסת ה-PHP שלך או לעבור למחשב מארח עם עם גירסת PHP חדשה! <br/>
-(במקרים של גרסת 5.0.x תוכל גם לרדת בגרסה ל- 4.4.x)
+(במקרים של גרסת 5.0 תוכל גם לרדת בגרסה ל- 4.4)
 </p>';
 $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
 $string['welcomep20'] = 'הינך רואה את עמוד זה מפני שהתקנת והפעלת בהלכה את <strong>{$a->packname} {$a->packversion}</strong>
index 46b031b..7ce183a 100644 (file)
@@ -33,4 +33,4 @@ defined('MOODLE_INTERNAL') || die();
 $string['language'] = 'שפת ממשק';
 $string['next'] = 'הבא';
 $string['previous'] = 'קודם';
-$string['reload'] = '×\98×¢×\9f מחדש';
+$string['reload'] = '×\98×¢×\99× ×\94 מחדש';
index 12ae97a..a4b6a21 100644 (file)
@@ -54,7 +54,7 @@ $string['paths'] = 'Caminhos';
 $string['pathserrcreatedataroot'] = 'O programa de instalação não conseguiu criar a pasta de dados <b>{$a->dataroot}</b>.';
 $string['pathshead'] = 'Confirmar caminhos';
 $string['pathsrodataroot'] = 'A pasta de dados não tem permissões de escrita.';
-$string['pathsroparentdataroot'] = 'A pasta pai <b>{$a->parent}</b> não tem permissões de escrita. O programa de instalação não conseguiu criar a pasta <b>{$a->dataroot}</b>.';
+$string['pathsroparentdataroot'] = 'A pasta ascendente <b>{$a->parent}</b> não tem permissões de escrita. O programa de instalação não conseguiu criar a pasta <b>{$a->dataroot}</b>.';
 $string['pathssubadmindir'] = 'Alguns servidores Web utilizam a pasta <strong>admin</strong> em URLs especiais de acesso a funcionalidades especiais, como é o caso de painéis de controlo. Algumas situações podem criar conflitos com a localização normal das páginas de administração do Moodle. Estes problemas podem ser resolvidos renomeando a pasta <strong>admin</strong> na instalação do Moodle e indicando aqui o novo nome a utilizar. Por exemplo:<br /><br /><b>moodleadmin</b><br /><br />Esta ação resolverá os problemas de acesso das hiperligações para as funcionalidades de administração do Moodle.';
 $string['pathssubdataroot'] = '<p>Uma diretoria em que o Moodle irá armazenar todo o conteúdo de ficheiros enviados pelos utilizadores.</p>
 <p>Esta diretoria deve ser legível e gravável pelo utilizador do servidor web (geralmente \'www-data\', \'nobody\', ou \'apache\').</p>
index 6e8f415..93db2e5 100644 (file)
@@ -224,6 +224,7 @@ $string['duplicateroleshortname'] = 'There is already a role with this short nam
 $string['duplicateusername'] = 'Duplicate username - skipping record';
 $string['emailfail'] = 'Emailing failed';
 $string['error'] = 'Error occurred';
+$string['error_question_answers_missing_in_db'] = 'Failed to find an answer matching "{$a->answer}" in the question_answers database table. This occurred while restoring the question with id {$a->filequestionid} in the backup file, which has been matched to the existing question with id {$a->dbquestionid} in the database.';
 $string['errorprocessingarchive'] = 'Error processing archive file';
 $string['errorcleaningdirectory'] = 'Error cleaning directory "{$a}"';
 $string['errorcopyingfiles'] = 'Error copying files';
index 3ca51ec..e1e346a 100644 (file)
@@ -105,6 +105,7 @@ define(['core/config'], function(config) {
     var hashString = function(source) {
         // From http://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript-jquery.
         /* jshint bitwise: false */
+        /* eslint no-bitwise: "off" */
         var hash = 0, i, chr, len;
         if (source.length === 0) {
             return hash;
index af58326..94959f0 100644 (file)
@@ -26,5 +26,6 @@
 define(function() {
 
     // This module exposes only the global yui instance.
+    /* global Y */
     return /** @alias module:core/yui */ Y;
 });
index bf1678b..5a1fd4d 100644 (file)
@@ -136,7 +136,12 @@ class memcached extends handler {
         ini_set('memcached.sess_locking', '1'); // Locking is required!
 
         // Try to configure lock and expire timeouts - ignored if memcached is before version 2.2.0.
-        ini_set('memcached.sess_lock_max_wait', $this->acquiretimeout);
+        if (version_compare($version, '3.0.0-dev') >= 0) {
+            ini_set('memcached.sess_lock_wait_max', $this->acquiretimeout * 1000);
+        } else {
+            ini_set('memcached.sess_lock_max_wait', $this->acquiretimeout);
+        }
+
         ini_set('memcached.sess_lock_expire', $this->lockexpire);
     }
 
index 6626075..a31117c 100644 (file)
@@ -27,156 +27,195 @@ define('AJAX_SCRIPT', true);
 require_once(dirname(__FILE__) . '/../../../config.php');
 require_once($CFG->libdir . '/filestorage/file_storage.php');
 
-$contextid = required_param('contextid', PARAM_INT);
-$elementid = required_param('elementid', PARAM_ALPHANUMEXT);
-$pagehash = required_param('pagehash', PARAM_ALPHANUMEXT);
-$pageinstance = required_param('pageinstance', PARAM_ALPHANUMEXT);
+// Clean up actions.
+$actions = array_map(function($actionparams) {
+    $action = isset($actionparams['action']) ? $actionparams['action'] : null;
+    $params = [];
+    $keys = [
+        'action' => PARAM_ALPHA,
+        'contextid' => PARAM_INT,
+        'elementid' => PARAM_ALPHANUMEXT,
+        'pagehash' => PARAM_ALPHANUMEXT,
+        'pageinstance' => PARAM_ALPHANUMEXT
+    ];
+
+    if ($action == 'save') {
+        $keys['drafttext'] = PARAM_RAW;
+    } else if ($action == 'resume') {
+        $keys['draftid'] = PARAM_INT;
+    }
+
+    foreach ($keys as $key => $type) {
+        // Replicate required_param().
+        if (!isset($actionparams[$key])) {
+            print_error('missingparam', '', '', $key);
+        }
+        $params[$key] = clean_param($actionparams[$key], $type);
+    }
+
+    return $params;
+}, isset($_REQUEST['actions']) ? $_REQUEST['actions'] : []);
+
 $now = time();
 // This is the oldest time any autosave text will be recovered from.
 // This is so that there is a good chance the draft files will still exist (there are many variables so
 // this is impossible to guarantee).
 $before = $now - 60*60*24*4;
 
-list($context, $course, $cm) = get_context_info_array($contextid);
+$context = context_system::instance();
 $PAGE->set_url('/lib/editor/atto/autosave-ajax.php');
 $PAGE->set_context($context);
 
-require_login($course, false, $cm);
-require_sesskey();
-
+require_login();
 if (isguestuser()) {
     print_error('accessdenied', 'admin');
 }
+require_sesskey();
 
 if (!in_array('atto', explode(',', get_config('core', 'texteditors')))) {
     print_error('accessdenied', 'admin');
 }
 
-$action = required_param('action', PARAM_ALPHA);
+$responses = array();
+foreach ($actions as $actionparams) {
+
+    $action = $actionparams['action'];
+    $contextid = $actionparams['contextid'];
+    $elementid = $actionparams['elementid'];
+    $pagehash = $actionparams['pagehash'];
+    $pageinstance = $actionparams['pageinstance'];
+
+    if ($action === 'save') {
+        $drafttext = $actionparams['drafttext'];
+        $params = array('elementid' => $elementid,
+                        'userid' => $USER->id,
+                        'pagehash' => $pagehash,
+                        'contextid' => $contextid);
+
+        $record = $DB->get_record('editor_atto_autosave', $params);
+        if ($record && $record->pageinstance != $pageinstance) {
+            print_error('concurrent access from the same user is not supported');
+            die();
+        }
 
-$response = array();
+        if (!$record) {
+            $record = new stdClass();
+            $record->elementid = $elementid;
+            $record->userid = $USER->id;
+            $record->pagehash = $pagehash;
+            $record->contextid = $contextid;
+            $record->drafttext = $drafttext;
+            $record->pageinstance = $pageinstance;
+            $record->timemodified = $now;
 
-if ($action === 'save') {
-    $drafttext = required_param('drafttext', PARAM_RAW);
-    $params = array('elementid' => $elementid,
-                    'userid' => $USER->id,
-                    'pagehash' => $pagehash,
-                    'contextid' => $contextid);
+            $DB->insert_record('editor_atto_autosave', $record);
 
-    $record = $DB->get_record('editor_atto_autosave', $params);
-    if ($record && $record->pageinstance != $pageinstance) {
-        print_error('concurrent access from the same user is not supported');
-        die();
-    }
+            // No response means no error.
+            $responses[] = null;
+            continue;
+        } else {
+            $record->drafttext = $drafttext;
+            $record->timemodified = time();
+            $DB->update_record('editor_atto_autosave', $record);
 
-    if (!$record) {
-        $record = new stdClass();
-        $record->elementid = $elementid;
-        $record->userid = $USER->id;
-        $record->pagehash = $pagehash;
-        $record->contextid = $contextid;
-        $record->drafttext = $drafttext;
-        $record->pageinstance = $pageinstance;
-        $record->timemodified = $now;
-
-        $DB->insert_record('editor_atto_autosave', $record);
-
-        // No response means no error.
-        die();
-    } else {
-        $record->drafttext = $drafttext;
-        $record->timemodified = time();
-        $DB->update_record('editor_atto_autosave', $record);
-
-        // No response means no error.
-        die();
-    }
-} else if ($action == 'resume') {
-    $params = array('elementid' => $elementid,
-                    'userid' => $USER->id,
-                    'pagehash' => $pagehash,
-                    'contextid' => $contextid);
-
-    $newdraftid = required_param('draftid', PARAM_INT);
-
-    $record = $DB->get_record('editor_atto_autosave', $params);
-
-    if (!$record) {
-        $record = new stdClass();
-        $record->elementid = $elementid;
-        $record->userid = $USER->id;
-        $record->pagehash = $pagehash;
-        $record->contextid = $contextid;
-        $record->pageinstance = $pageinstance;
-        $record->pagehash = $pagehash;
-        $record->draftid = $newdraftid;
-        $record->timemodified = time();
-        $record->drafttext = '';
-
-        $DB->insert_record('editor_atto_autosave', $record);
-
-        // No response means no error.
-        die();
-    } else {
-        // Copy all draft files from the old draft area.
-        $usercontext = context_user::instance($USER->id);
-        $stale = $record->timemodified < $before;
-        require_once($CFG->libdir . '/filelib.php');
-
-        $fs = get_file_storage();
-        $files = $fs->get_directory_files($usercontext->id, 'user', 'draft', $newdraftid, '/', true, true);
-
-        $lastfilemodified = 0;
-        foreach ($files as $file) {
-            $lastfilemodified = max($lastfilemodified, $file->get_timemodified());
-        }
-        if ($record->timemodified < $lastfilemodified) {
-            $stale = true;
+            // No response means no error.
+            $responses[] = null;
+            continue;
         }
 
-        if (!$stale) {
-            // This function copies all the files in one draft area, to another area (in this case it's
-            // another draft area). It also rewrites the text to @@PLUGINFILE@@ links.
-            $newdrafttext = file_save_draft_area_files($record->draftid,
-                                                       $usercontext->id,
-                                                       'user',
-                                                       'draft',
-                                                       $newdraftid,
-                                                       array(),
-                                                       $record->drafttext);
-
-            // Final rewrite to the new draft area (convert the @@PLUGINFILES@@ again).
-            $newdrafttext = file_rewrite_pluginfile_urls($newdrafttext,
-                                                         'draftfile.php',
-                                                         $usercontext->id,
-                                                         'user',
-                                                         'draft',
-                                                         $newdraftid);
-            $record->drafttext = $newdrafttext;
+    } else if ($action == 'resume') {
+        $params = array('elementid' => $elementid,
+                        'userid' => $USER->id,
+                        'pagehash' => $pagehash,
+                        'contextid' => $contextid);
+
+        $newdraftid = $actionparams['draftid'];
+
+        $record = $DB->get_record('editor_atto_autosave', $params);
 
+        if (!$record) {
+            $record = new stdClass();
+            $record->elementid = $elementid;
+            $record->userid = $USER->id;
+            $record->pagehash = $pagehash;
+            $record->contextid = $contextid;
             $record->pageinstance = $pageinstance;
+            $record->pagehash = $pagehash;
             $record->draftid = $newdraftid;
             $record->timemodified = time();
-            $DB->update_record('editor_atto_autosave', $record);
+            $record->drafttext = '';
 
-            // A response means the draft has been restored and here is the auto-saved text.
-            $response['result'] = $record->drafttext;
-            echo json_encode($response);
-        } else {
-            $DB->delete_records('editor_atto_autosave', array('id' => $record->id));
+            $DB->insert_record('editor_atto_autosave', $record);
 
             // No response means no error.
+            $responses[] = null;
+            continue;
+
+        } else {
+            // Copy all draft files from the old draft area.
+            $usercontext = context_user::instance($USER->id);
+            $stale = $record->timemodified < $before;
+            require_once($CFG->libdir . '/filelib.php');
+
+            $fs = get_file_storage();
+            $files = $fs->get_directory_files($usercontext->id, 'user', 'draft', $newdraftid, '/', true, true);
+
+            $lastfilemodified = 0;
+            foreach ($files as $file) {
+                $lastfilemodified = max($lastfilemodified, $file->get_timemodified());
+            }
+            if ($record->timemodified < $lastfilemodified) {
+                $stale = true;
+            }
+
+            if (!$stale) {
+                // This function copies all the files in one draft area, to another area (in this case it's
+                // another draft area). It also rewrites the text to @@PLUGINFILE@@ links.
+                $newdrafttext = file_save_draft_area_files($record->draftid,
+                                                           $usercontext->id,
+                                                           'user',
+                                                           'draft',
+                                                           $newdraftid,
+                                                           array(),
+                                                           $record->drafttext);
+
+                // Final rewrite to the new draft area (convert the @@PLUGINFILES@@ again).
+                $newdrafttext = file_rewrite_pluginfile_urls($newdrafttext,
+                                                             'draftfile.php',
+                                                             $usercontext->id,
+                                                             'user',
+                                                             'draft',
+                                                             $newdraftid);
+                $record->drafttext = $newdrafttext;
+
+                $record->pageinstance = $pageinstance;
+                $record->draftid = $newdraftid;
+                $record->timemodified = time();
+                $DB->update_record('editor_atto_autosave', $record);
+
+                // A response means the draft has been restored and here is the auto-saved text.
+                $response = ['result' => $record->drafttext];
+                $responses[] = $response;
+
+            } else {
+                $DB->delete_records('editor_atto_autosave', array('id' => $record->id));
+
+                // No response means no error.
+                $responses[] = null;
+            }
+            continue;
         }
-        die();
+
+    } else if ($action == 'reset') {
+        $params = array('elementid' => $elementid,
+                        'userid' => $USER->id,
+                        'pagehash' => $pagehash,
+                        'contextid' => $contextid);
+
+        $DB->delete_records('editor_atto_autosave', $params);
+        $responses[] = null;
+        continue;
     }
-} else if ($action == 'reset') {
-    $params = array('elementid' => $elementid,
-                    'userid' => $USER->id,
-                    'pagehash' => $pagehash,
-                    'contextid' => $contextid);
-
-    $DB->delete_records('editor_atto_autosave', $params);
-    die();
 }
 
-print_error('invalidarguments');
+echo json_encode($responses);
index 09ba0b7..fbcfd8b 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js differ
index 912122e..c8557d0 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js differ
index 27f74d3..d2b7ea5 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js differ
index 3984d2f..cd310b4 100644 (file)
@@ -7,6 +7,7 @@
                 "notify.js",
                 "textarea.js",
                 "autosave.js",
+                "autosave-io.js",
                 "clean.js",
                 "commands.js",
                 "toolbar.js",
diff --git a/lib/editor/atto/yui/src/editor/js/autosave-io.js b/lib/editor/atto/yui/src/editor/js/autosave-io.js
new file mode 100644 (file)
index 0000000..a37352e
--- /dev/null
@@ -0,0 +1,244 @@
+// 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 autosave function for the Atto editor.
+ *
+ * @module     moodle-editor_atto-autosave-io
+ * @submodule  autosave-io
+ * @package    editor_atto
+ * @copyright  2016 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+var EditorAutosaveIoDispatcherInstance = null;
+
+function EditorAutosaveIoDispatcher() {
+    EditorAutosaveIoDispatcher.superclass.constructor.apply(this, arguments);
+    this._submitEvents = {};
+    this._queue = [];
+    this._throttle = null;
+}
+EditorAutosaveIoDispatcher.NAME = 'EditorAutosaveIoDispatcher';
+EditorAutosaveIoDispatcher.ATTRS = {
+
+    /**
+     * The relative path to the ajax script.
+     *
+     * @attribute autosaveAjaxScript
+     * @type String
+     * @default '/lib/editor/atto/autosave-ajax.php'
+     * @readOnly
+     */
+    autosaveAjaxScript: {
+        value: '/lib/editor/atto/autosave-ajax.php',
+        readOnly: true
+    },
+
+    /**
+     * The time buffer for the throttled requested.
+     *
+     * @attribute delay
+     * @type Number
+     * @default 50
+     * @readOnly
+     */
+    delay: {
+        value: 50,
+        readOnly: true
+    }
+
+};
+Y.extend(EditorAutosaveIoDispatcher, Y.Base, {
+
+    /**
+     * Dispatch an IO request.
+     *
+     * This method will put the requests in a queue in order to attempt to bulk them.
+     *
+     * @param  {Object} params    The parameters of the request.
+     * @param  {Object} context   The context in which the callbacks are called.
+     * @param  {Object} callbacks Object with 'success', 'complete', 'end', 'failure' and 'start' as
+     *                            optional keys defining the callbacks to call. Success and Complete
+     *                            functions will receive the response as parameter. Success and Complete
+     *                            may receive an object containing the error key, use this to confirm
+     *                            that no errors occured.
+     * @return {Void}
+     */
+    dispatch: function(params, context, callbacks) {
+        if (this._throttle) {
+            this._throttle.cancel();
+        }
+
+        this._throttle = Y.later(this.get('delay'), this, this._processDispatchQueue);
+        this._queue.push([params, context, callbacks]);
+    },
+
+    /**
+     * Dispatches the requests in the queue.
+     *
+     * @return {Void}
+     */
+    _processDispatchQueue: function() {
+        var queue = this._queue,
+            data = {};
+
+        this._queue = [];
+        if (queue.length < 1) {
+            return;
+        }
+
+        Y.Array.each(queue, function(item, index) {
+            data[index] = item[0];
+        });
+
+        Y.io(M.cfg.wwwroot + this.get('autosaveAjaxScript'), {
+            method: 'POST',
+            data: Y.QueryString.stringify({
+                actions: data,
+                sesskey: M.cfg.sesskey
+            }),
+            on: {
+                start: this._makeIoEventCallback('start', queue),
+                complete: this._makeIoEventCallback('complete', queue),
+                failure: this._makeIoEventCallback('failure', queue),
+                end: this._makeIoEventCallback('end', queue),
+                success: this._makeIoEventCallback('success', queue)
+            }
+        });
+    },
+
+    /**
+     * Creates a function that dispatches an IO response to callbacks.
+     *
+     * @param  {String} event The type of event.
+     * @param  {Array} queue The queue.
+     * @return {Function}
+     */
+    _makeIoEventCallback: function(event, queue) {
+        var noop = function() {};
+        return function() {
+            var response = arguments[1],
+                parsed = {};
+
+            if ((event == 'complete' || event == 'success') && (typeof response !== 'undefined'
+                    && typeof response.responseText !== 'undefined' && response.responseText !== '')) {
+
+                // Success and complete events need to parse the response.
+                parsed = JSON.parse(response.responseText) || {};
+            }
+
+            Y.Array.each(queue, function(item, index) {
+                var context = item[1],
+                    cb = (item[2] && item[2][event]) || noop,
+                    arg;
+
+                if (parsed && parsed.error) {
+                    // The response is an error, we send it to everyone.
+                    arg = parsed;
+                } else if (parsed) {
+                    // The response was parsed, we only communicate the relevant portion of the response.
+                    arg = parsed[index];
+                }
+
+                cb.apply(context, [arg]);
+            });
+        };
+    },
+
+    /**
+     * Form submit handler.
+     *
+     * @param  {EventFacade} e The event.
+     * @return {Void}
+     */
+    _onSubmit: function(e) {
+        var data = {},
+            id = e.currentTarget.generateID(),
+            params = this._submitEvents[id];
+
+        if (!params || params.ios.length < 1) {
+            return;
+        }
+
+        Y.Array.each(params.ios, function(param, index) {
+            data[index] = param;
+        });
+
+        Y.io(M.cfg.wwwroot + this.get('autosaveAjaxScript'), {
+            method: 'POST',
+            data: Y.QueryString.stringify({
+                actions: data,
+                sesskey: M.cfg.sesskey
+            }),
+            sync: true
+        });
+    },
+
+    /**
+     * Registers a request to be made on form submission.
+     *
+     * @param  {Node} node The forum node we will listen to.
+     * @param  {Object} params Parameters for the IO request.
+     * @return {Void}
+     */
+    whenSubmit: function(node, params) {
+        if (typeof this._submitEvents[node.generateID()] === 'undefined') {
+            this._submitEvents[node.generateID()] = {
+                event: node.on('submit', this._onSubmit, this),
+                ios: []
+            };
+        }
+        this._submitEvents[node.get('id')].ios.push([params]);
+    }
+
+});
+EditorAutosaveIoDispatcherInstance = new EditorAutosaveIoDispatcher();
+
+
+function EditorAutosaveIo() {}
+EditorAutosaveIo.prototype = {
+
+    /**
+     * Dispatch an IO request.
+     *
+     * This method will put the requests in a queue in order to attempt to bulk them.
+     *
+     * @param  {Object} params    The parameters of the request.
+     * @param  {Object} context   The context in which the callbacks are called.
+     * @param  {Object} callbacks Object with 'success', 'complete', 'end', 'failure' and 'start' as
+     *                            optional keys defining the callbacks to call. Success and Complete
+     *                            functions will receive the response as parameter. Success and Complete
+     *                            may receive an object containing the error key, use this to confirm
+     *                            that no errors occured.
+     * @return {Void}
+     */
+    autosaveIo: function(params, context, callbacks) {
+        EditorAutosaveIoDispatcherInstance.dispatch(params, context, callbacks);
+    },
+
+    /**
+     * Registers a request to be made on form submission.
+     *
+     * @param  {Node} form The forum node we will listen to.
+     * @param  {Object} params Parameters for the IO request.
+     * @return {Void}
+     */
+    autosaveIoOnSubmit: function(form, params) {
+        EditorAutosaveIoDispatcherInstance.whenSubmit(form, params);
+    }
+
+};
+Y.Base.mix(Y.M.editor_atto.Editor, [EditorAutosaveIo]);
index 4dd4ed4..27f227a 100644 (file)
@@ -65,19 +65,6 @@ EditorAutosave.ATTRS= {
     pageHash: {
         value: '',
         writeOnce: true
-    },
-
-    /**
-     * The relative path to the ajax script.
-     *
-     * @attribute autosaveAjaxScript
-     * @type String
-     * @default '/lib/editor/atto/autosave-ajax.php'
-     * @readOnly
-     */
-    autosaveAjaxScript: {
-        value: '/lib/editor/atto/autosave-ajax.php',
-        readOnly: true
     }
 };
 
@@ -118,8 +105,7 @@ EditorAutosave.prototype = {
             form,
             optiontype = null,
             options = this.get('filepickeroptions'),
-            params,
-            url;
+            params;
 
         if (!this.get('autosaveEnabled')) {
             // Autosave disabled for this instance.
@@ -135,99 +121,73 @@ EditorAutosave.prototype = {
 
         // First see if there are any saved drafts.
         // Make an ajax request.
-        url = M.cfg.wwwroot + this.get('autosaveAjaxScript');
         params = {
-            sesskey: M.cfg.sesskey,
             contextid: this.get('contextid'),
             action: 'resume',
-            drafttext: '',
             draftid: draftid,
             elementid: this.get('elementid'),
             pageinstance: this.autosaveInstance,
             pagehash: this.get('pageHash')
         };
 
-        Y.io(url, {
-            method: 'POST',
-            data: params,
-            context: this,
-            on: {
-                success: function(id,o) {
-                    var response_json;
-                    if (typeof o.responseText !== "undefined" && o.responseText !== "") {
-                        response_json = JSON.parse(o.responseText);
+        this.autosaveIo(params, this, {
+            success: function(response) {
+                if (response === null) {
+                    // This can happen when there is nothing to resume from.
+                    return;
+                } else if (!response) {
+                    Y.log('Invalid response received.', 'debug', LOGNAME_AUTOSAVE);
+                    return;
+                }
 
-                        // Revert untouched editor contents to an empty string.
-                        // Check for FF and Chrome.
-                        if (response_json.result === '<p></p>' || response_json.result === '<p><br></p>' ||
-                            response_json.result === '<br>') {
-                            response_json.result = '';
-                        }
+                // Revert untouched editor contents to an empty string.
+                // Check for FF and Chrome.
+                if (response.result === '<p></p>' || response.result === '<p><br></p>' ||
+                    response.result === '<br>') {
+                    response.result = '';
+                }
 
-                        // Check for IE 9 and 10.
-                        if (response_json.result === '<p>&nbsp;</p>' || response_json.result === '<p><br>&nbsp;</p>') {
-                            response_json.result = '';
-                        }
+                // Check for IE 9 and 10.
+                if (response.result === '<p>&nbsp;</p>' || response.result === '<p><br>&nbsp;</p>') {
+                    response.result = '';
+                }
 
-                        if (response_json.error || typeof response_json.result === 'undefined') {
-                            Y.log('Error occurred recovering draft text: ' + response_json.error, 'debug', LOGNAME_AUTOSAVE);
-                            this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'),
-                                    NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
-                        } else if (response_json.result !== this.textarea.get('value') &&
-                                response_json.result !== '') {
-                            Y.log('Autosave text found - recover it.', 'debug', LOGNAME_AUTOSAVE);
-                            this.recoverText(response_json.result);
-                        }
-                        this._fireSelectionChanged();
-                    }
-                },
-                failure: function() {
+                if (response.error || typeof response.result === 'undefined') {
+                    Y.log('Error occurred recovering draft text: ' + response.error, 'debug', LOGNAME_AUTOSAVE);
                     this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'),
                             NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
+                } else if (response.result !== this.textarea.get('value') &&
+                        response.result !== '') {
+                    Y.log('Autosave text found - recover it.', 'debug', LOGNAME_AUTOSAVE);
+                    this.recoverText(response.result);
                 }
+                this._fireSelectionChanged();
+
+            },
+            failure: function() {
+                this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'),
+                        NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
             }
         });
 
         // Now setup the timer for periodic saves.
-
         var delay = parseInt(this.get('autosaveFrequency'), 10) * 1000;
         this.autosaveTimer = Y.later(delay, this, this.saveDraft, false, true);
 
         // Now setup the listener for form submission.
         form = this.textarea.ancestor('form');
         if (form) {
-            form.on('submit', this.resetAutosave, this);
+            this.autosaveIoOnSubmit(form, {
+                action: 'reset',
+                contextid: this.get('contextid'),
+                elementid: this.get('elementid'),
+                pageinstance: this.autosaveInstance,
+                pagehash: this.get('pageHash')
+            });
         }
         return this;
     },
 
-    /**
-     * Clear the autosave text because the form was submitted normally.
-     *
-     * @method resetAutosave
-     * @chainable
-     */
-    resetAutosave: function() {
-        // Make an ajax request to reset the autosaved text.
-        var url = M.cfg.wwwroot + this.get('autosaveAjaxScript');
-        var params = {
-            sesskey: M.cfg.sesskey,
-            contextid: this.get('contextid'),
-            action: 'reset',
-            elementid: this.get('elementid'),
-            pageinstance: this.autosaveInstance,
-            pagehash: this.get('pageHash')
-        };
-
-        Y.io(url, {
-            method: 'POST',
-            data: params,
-            sync: true
-        });
-        return this;
-    },
-
-
     /**
      * Recover a previous version of this text and show a message.
      *
@@ -283,29 +243,23 @@ EditorAutosave.prototype = {
             };
 
             // Reusable error handler - must be passed the correct context.
-            var ajaxErrorFunction = function(code, response) {
+            var ajaxErrorFunction = function(response) {
                 var errorDuration = parseInt(this.get('autosaveFrequency'), 10) * 1000;
-                Y.log('Error while autosaving text:' + code, 'warn', LOGNAME_AUTOSAVE);
+                Y.log('Error while autosaving text', 'warn', LOGNAME_AUTOSAVE);
                 Y.log(response, 'warn', LOGNAME_AUTOSAVE);
                 this.showMessage(M.util.get_string('autosavefailed', 'editor_atto'), NOTIFY_WARNING, errorDuration);
             };
 
-            Y.io(url, {
-                method: 'POST',
-                data: params,
-                context: this,
-                on: {
-                    error: ajaxErrorFunction,
-                    failure: ajaxErrorFunction,
-                    success: function(code, response) {
-                        if (response.responseText !== "") {
-                            Y.soon(Y.bind(ajaxErrorFunction, this, [code, response]));
-                        } else {
-                            // All working.
-                            this.lastText = newText;
-                            this.showMessage(M.util.get_string('autosavesucceeded', 'editor_atto'),
-                                    NOTIFY_INFO, SUCCESS_MESSAGE_TIMEOUT);
-                        }
+            this.autosaveIo(params, this, {
+                failure: ajaxErrorFunction,
+                success: function(response) {
+                    if (response && response.error) {
+                        Y.soon(Y.bind(ajaxErrorFunction, this, [response]));
+                    } else {
+                        // All working.
+                        this.lastText = newText;
+                        this.showMessage(M.util.get_string('autosavesucceeded', 'editor_atto'),
+                                NOTIFY_INFO, SUCCESS_MESSAGE_TIMEOUT);
                     }
                 }
             });
index a8f9ff3..65750e3 100644 (file)
@@ -15,7 +15,8 @@
             "moodle-core-notification-confirm",
             "moodle-editor_atto-rangy",
             "handlebars",
-            "timers"
+            "timers",
+            "querystring-stringify"
         ]
     },
     "moodle-editor_atto-plugin": {
index 3a66e69..7f9bab6 100644 (file)
@@ -128,6 +128,9 @@ class MoodleQuickForm_tags extends MoodleQuickForm_autocomplete {
      */
     public function onQuickFormEvent($event, $arg, &$caller) {
         if ($event === 'createElement') {
+            if (!is_array($arg[2])) {
+                $arg[2] = [];
+            }
             $arg[2] += array('itemtype' => '', 'component' => '');
         }
         return parent::onQuickFormEvent($event, $arg, $caller);
index af0c977..d0d5b23 100644 (file)
@@ -566,7 +566,7 @@ class core_renderer extends renderer_base {
     public function standard_top_of_body_html() {
         global $CFG;
         $output = $this->page->requires->get_top_of_body_code();
-        if (!empty($CFG->additionalhtmltopofbody)) {
+        if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmltopofbody)) {
             $output .= "\n".$CFG->additionalhtmltopofbody;
         }
         $output .= $this->maintenance_warning();
@@ -689,7 +689,7 @@ class core_renderer extends renderer_base {
         // but some of the content won't be known until later, so we return a placeholder
         // for now. This will be replaced with the real content in {@link core_renderer::footer()}.
         $output = '';
-        if (!empty($CFG->additionalhtmlfooter)) {
+        if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmlfooter)) {
             $output .= "\n".$CFG->additionalhtmlfooter;
         }
         $output .= $this->unique_end_html_token;
index c2a9862..0b95413 100644 (file)
@@ -429,8 +429,19 @@ class behat_hooks extends behat_base {
             return false;
         }
 
-        list ($dir, $filename) = $this->get_faildump_filename($scope, 'png');
-        $this->saveScreenshot($filename, $dir);
+        // Some drivers (e.g. chromedriver) may throw an exception while trying to take a screenshot.  If this isn't handled,
+        // the behat run dies.  We don't want to lose the information about the failure that triggered the screenshot,
+        // so let's log the exception message to a file (to explain why there's no screenshot) and allow the run to continue,
+        // handling the failure as normal.
+        try {
+            list ($dir, $filename) = $this->get_faildump_filename($scope, 'png');
+            $this->saveScreenshot($filename, $dir);
+        } catch (Exception $e) {
+            // Catching all exceptions as we don't know what the driver might throw.
+            list ($dir, $filename) = $this->get_faildump_filename($scope, 'txt');
+            $message = "Could not save screenshot due to an error\n" . $e->getMessage();
+            file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $message);
+        }
     }
 
     /**
@@ -442,9 +453,14 @@ class behat_hooks extends behat_base {
     protected function take_contentdump(AfterStepScope $scope) {
         list ($dir, $filename) = $this->get_faildump_filename($scope, 'html');
 
-        $fh = fopen($dir . DIRECTORY_SEPARATOR . $filename, 'w');
-        fwrite($fh, $this->getSession()->getPage()->getContent());
-        fclose($fh);
+        try {
+            // Driver may throw an exception during getContent(), so do it first to avoid getting an empty file.
+            $content = $this->getSession()->getPage()->getContent();
+        } catch (Exception $e) {
+            // Catching all exceptions as we don't know what the driver might throw.
+            $content = "Could not save contentdump due to an error\n" . $e->getMessage();
+        }
+        file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $content);
     }
 
     /**
index a7aa3bf..d0be09f 100644 (file)
@@ -115,36 +115,47 @@ class core_weblib_format_text_testcase extends advanced_testcase {
      */
     public function format_text_blanktarget_testcases() {
         return [
-            'Simple link' =>
-                [
-                    '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4">Hey, that\'s pretty good!</a>',
-                    '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_blank"' .
-                        ' rel="noreferrer">Hey, that\'s pretty good!</a></div>'
-                ],
-            'Link with rel' =>
-                [
-                    '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="nofollow">Hey, that\'s pretty good!</a>',
-                    '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="nofollow noreferrer"' .
-                        ' target="_blank">Hey, that\'s pretty good!</a></div>'
-                ],
-            'Link with rel noreferrer' =>
-                [
-                    '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="noreferrer">Hey, that\'s pretty good!</a>',
-                    '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="noreferrer"' .
-                     ' target="_blank">Hey, that\'s pretty good!</a></div>'
-                ],
-            'Link with target' =>
-                [
-                    '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_self">Hey, that\'s pretty good!</a>',
-                    '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_self">' .
-                        'Hey, that\'s pretty good!</a></div>'
-                ],
-            'Link with target blank' =>
-                [
-                    '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_blank">Hey, that\'s pretty good!</a>',
-                    '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_blank"' .
-                        ' rel="noreferrer">Hey, that\'s pretty good!</a></div>'
-                ]
+            'Simple link' => [
+                '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4">Hey, that\'s pretty good!</a>',
+                '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_blank"' .
+                    ' rel="noreferrer">Hey, that\'s pretty good!</a></div>'
+            ],
+            'Link with rel' => [
+                '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="nofollow">Hey, that\'s pretty good!</a>',
+                '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="nofollow noreferrer"' .
+                    ' target="_blank">Hey, that\'s pretty good!</a></div>'
+            ],
+            'Link with rel noreferrer' => [
+                '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="noreferrer">Hey, that\'s pretty good!</a>',
+                '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="noreferrer"' .
+                 ' target="_blank">Hey, that\'s pretty good!</a></div>'
+            ],
+            'Link with target' => [
+                '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_self">Hey, that\'s pretty good!</a>',
+                '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_self">' .
+                    'Hey, that\'s pretty good!</a></div>'
+            ],
+            'Link with target blank' => [
+                '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_blank">Hey, that\'s pretty good!</a>',
+                '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_blank"' .
+                    ' rel="noreferrer">Hey, that\'s pretty good!</a></div>'
+            ],
+            'Link with Frank\'s casket inscription' => [
+                '<a href="https://en.wikipedia.org/wiki/Franks_Casket">ᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻ' .
+                    'ᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁ</a>',
+                '<div class="text_to_html"><a href="https://en.wikipedia.org/wiki/Franks_Casket" target="_blank" ' .
+                    'rel="noreferrer">ᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾ' .
+                    'ᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁ</a></div>'
+             ],
+            'No link' => [
+                'Some very boring text written with the Latin script',
+                '<div class="text_to_html">Some very boring text written with the Latin script</div>'
+            ],
+            'No link with Thror\'s map runes' => [
+                'ᛋᛏᚫᚾᛞ ᛒᚣ ᚦᛖ ᚷᚱᛖᚣ ᛋᛏᚩᚾᛖ ᚻᚹᛁᛚᛖ ᚦᛖ ᚦᚱᚢᛋᚻ ᚾᚩᚳᛋ ᚫᚾᛞ ᚦᛖ ᛋᛖᛏᛏᛁᚾᚷ ᛋᚢᚾ ᚹᛁᚦ ᚦᛖ ᛚᚫᛋᛏ ᛚᛁᚷᚻᛏ ᚩᚠ ᛞᚢᚱᛁᚾᛋ ᛞᚫᚣ ᚹᛁᛚᛚ ᛋᚻᛁᚾᛖ ᚢᛈᚩᚾ ᚦᛖ ᚳᛖᚣᚻᚩᛚᛖ',
+                '<div class="text_to_html">ᛋᛏᚫᚾᛞ ᛒᚣ ᚦᛖ ᚷᚱᛖᚣ ᛋᛏᚩᚾᛖ ᚻᚹᛁᛚᛖ ᚦᛖ ᚦᚱᚢᛋᚻ ᚾᚩᚳᛋ ᚫᚾᛞ ᚦᛖ ᛋᛖᛏᛏᛁᚾᚷ ᛋᚢᚾ ᚹᛁᚦ ᚦᛖ ᛚᚫᛋᛏ ᛚᛁᚷᚻᛏ ᚩᚠ ᛞᚢᚱᛁᚾᛋ ᛞᚫᚣ ᚹ' .
+                'ᛁᛚᛚ ᛋᚻᛁᚾᛖ ᚢᛈᚩᚾ ᚦᛖ ᚳᛖᚣᚻᚩᛚᛖ</div>'
+            ]
         ];
     }
 }
index e094828..c86dc0e 100644 (file)
@@ -1321,7 +1321,7 @@ function format_text($text, $format = FORMAT_MOODLE, $options = null, $courseidd
 
     if ($options['blanktarget']) {
         $domdoc = new DOMDocument();
-        $domdoc->loadHTML($text);
+        $domdoc->loadHTML('<?xml version="1.0" encoding="UTF-8" ?>' . $text);
         foreach ($domdoc->getElementsByTagName('a') as $link) {
             if ($link->hasAttribute('target') && strpos($link->getAttribute('target'), '_blank') === false) {
                 continue;
@@ -1336,7 +1336,7 @@ function format_text($text, $format = FORMAT_MOODLE, $options = null, $courseidd
         // $domdoc->loadHTML($text, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); however it seems like the libxml
         // version that travis uses doesn't work properly and ends up leaving <html><body>, so I'm forced to use
         // this regex to remove those tags.
-        $text = trim(preg_replace('~<(?:!DOCTYPE|/?(?:html|body))[^>]*>\s*~i', '', $domdoc->saveHTML()));
+        $text = trim(preg_replace('~<(?:!DOCTYPE|/?(?:html|body))[^>]*>\s*~i', '', $domdoc->saveHTML($domdoc->documentElement)));
     }
 
     return $text;
index 88d0a5f..6702c3b 100644 (file)
@@ -137,7 +137,8 @@ class core_message_external extends external_api {
             if ($success && empty($contactlist[$message['touserid']]) && !empty($blocknoncontacts)) {
                 // The user isn't a contact and they have selected to block non contacts so this message won't be sent.
                 $success = false;
-                $errormessage = get_string('userisblockingyounoncontact', 'message');
+                $errormessage = get_string('userisblockingyounoncontact', 'message',
+                        fullname(core_user::get_user($message['touserid'])));
             }
 
             //now we can send the message (at least try)
index 347dbdf..d872488 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js differ
index 084e92b..ebc14b8 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js differ
index 347dbdf..d872488 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js differ
index d4597ec..9d1c646 100644 (file)
@@ -42,7 +42,7 @@ Y.extend(ANNOTATIONSTAMP, M.assignfeedback_editpdf.annotation, {
      */
     draw : function() {
         var drawable = new M.assignfeedback_editpdf.drawable(this.editor),
-            drawingregion = this.editor.get_dialogue_element(SELECTOR.DRAWINGREGION),
+            drawingcanvas = this.editor.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
             node,
             position;
 
@@ -58,16 +58,10 @@ Y.extend(ANNOTATIONSTAMP, M.assignfeedback_editpdf.annotation, {
             'zIndex': 50
         });
 
-        drawingregion.append(node);
+        drawingcanvas.append(node);
         node.setX(position.x);
         node.setY(position.y);
         drawable.store_position(node, position.x, position.y);
-
-        // Pass throught the event handlers on the div.
-        node.on('gesturemovestart', this.editor.edit_start, null, this.editor);
-        node.on('gesturemove', this.editor.edit_move, null, this.editor);
-        node.on('gesturemoveend', this.editor.edit_end, null, this.editor);
-
         drawable.nodes.push(node);
 
         this.drawable = drawable;
index bdb96f9..53f91c7 100644 (file)
@@ -159,7 +159,7 @@ var COMMENT = function(editor, gradeid, pageno, x, y, width, colour, rawtext) {
     this.draw = function(focus) {
         var drawable = new M.assignfeedback_editpdf.drawable(this.editor),
             node,
-            drawingregion = this.editor.get_dialogue_element(SELECTOR.DRAWINGREGION),
+            drawingcanvas = this.editor.get_dialogue_element(SELECTOR.DRAWINGCANVAS),
             container,
             menu,
             position,
@@ -189,14 +189,14 @@ var COMMENT = function(editor, gradeid, pageno, x, y, width, colour, rawtext) {
             color: COMMENTTEXTCOLOUR
         });
 
-        drawingregion.append(container);
+        drawingcanvas.append(container);
         container.setStyle('position', 'absolute');
         container.setX(position.x);
         container.setY(position.y);
         drawable.store_position(container, position.x, position.y);
         drawable.nodes.push(container);
         node.set('value', this.rawtext);
-        scrollheight = node.get('scrollHeight'),
+        scrollheight = node.get('scrollHeight');
         node.setStyles({
             'height' : scrollheight + 'px',
             'overflow': 'hidden'
index 6912735..8d842f8 100644 (file)
@@ -1106,7 +1106,7 @@ EDITOR.prototype = {
         drawingcanvas.setStyle('height', page.height + 'px');
 
         // Update page select.
-        this.get_dialogue_element(SELECTOR.PAGESELECT).set('value', this.currentpage);
+        this.get_dialogue_element(SELECTOR.PAGESELECT).set('selectedIndex', this.currentpage);
 
         this.resize(); // Internally will call 'redraw', after checking the dialogue size.
     },
index c4ce567..cd23c8e 100644 (file)
@@ -1411,7 +1411,11 @@ function assign_get_completion_state($course, $cm, $userid, $type) {
 
     // If completion option is enabled, evaluate it and return true/false.
     if ($assign->get_instance()->completionsubmit) {
-        $submission = $assign->get_user_submission($userid, false);
+        if ($assign->get_instance()->teamsubmission) {
+            $submission = $assign->get_group_submission($userid, 0, false);
+        } else {
+            $submission = $assign->get_user_submission($userid, false);
+        }
         return $submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED;
     } else {
         // Completion option is not enabled so just return $type.
index c5a2b1e..5160225 100644 (file)
@@ -5466,11 +5466,10 @@ class assign {
      * @param int $updatetime
      * @return void
      */
-    public function send_notification($userfrom,
-                                      $userto,
-                                      $messagetype,
-                                      $eventtype,
-                                      $updatetime) {
+    public function send_notification($userfrom, $userto, $messagetype, $eventtype, $updatetime) {
+        global $USER;
+        $userid = core_user::is_real_user($userfrom->id) ? $userfrom->id : $USER->id;
+        $uniqueid = $this->get_uniqueid_for_user($userid);
         self::send_assignment_notification($userfrom,
                                            $userto,
                                            $messagetype,
@@ -5482,7 +5481,7 @@ class assign {
                                            $this->get_module_name(),
                                            $this->get_instance()->name,
                                            $this->is_blind_marking(),
-                                           $this->get_uniqueid_for_user($userfrom->id));
+                                           $uniqueid);
     }
 
     /**
@@ -5633,7 +5632,12 @@ class assign {
             $this->update_submission($submission, $userid, true, $instance->teamsubmission);
             $completion = new completion_info($this->get_course());
             if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
-                $completion->update_state($this->get_course_module(), COMPLETION_COMPLETE, $userid);
+                $this->update_activity_completion_records($instance->teamsubmission,
+                                                          $instance->requireallteammemberssubmit,
+                                                          $submission,
+                                                          $userid,
+                                                          COMPLETION_COMPLETE,
+                                                          $completion);
             }
 
             if (!empty($data->submissionstatement) && $USER->id == $userid) {
@@ -6325,7 +6329,12 @@ class assign {
         }
         $completion = new completion_info($this->get_course());
         if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
-            $completion->update_state($this->get_course_module(), $complete, $USER->id);
+            $this->update_activity_completion_records($instance->teamsubmission,
+                                                      $instance->requireallteammemberssubmit,
+                                                      $submission,
+                                                      $USER->id,
+                                                      $complete,
+                                                      $completion);
         }
 
         if (!$instance->submissiondrafts) {
@@ -7991,6 +8000,42 @@ class assign {
         }
         return $this->get_course_module()->id . '_' . $id;
     }
+
+    /**
+     * Updates and creates the completion records in mdl_course_modules_completion.
+     *
+     * @param int $teamsubmission value of 0 or 1 to indicate whether this is a group activity
+     * @param int $requireallteammemberssubmit value of 0 or 1 to indicate whether all group members must click Submit
+     * @param obj $submission the submission
+     * @param int $userid the user id
+     * @param int $complete
+     * @param obj $completion
+     *
+     * @return null
+     */
+    protected function update_activity_completion_records($teamsubmission,
+                                                          $requireallteammemberssubmit,
+                                                          $submission,
+                                                          $userid,
+                                                          $complete,
+                                                          $completion) {
+
+        if (($teamsubmission && $submission->groupid > 0 && !$requireallteammemberssubmit) ||
+            ($teamsubmission && $submission->groupid > 0 && $requireallteammemberssubmit &&
+             $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED)) {
+
+            $members = groups_get_members($submission->groupid);
+
+            foreach ($members as $member) {
+                $completion->update_state($this->get_course_module(), $complete, $member->id);
+            }
+        } else {
+            $completion->update_state($this->get_course_module(), $complete, $userid);
+        }
+
+        return;
+    }
+
 }
 
 /**
index 412f72f..be6166d 100644 (file)
@@ -209,32 +209,6 @@ class mod_assign_mod_form extends moodleform_mod {
         $this->apply_admin_defaults();
 
         $this->add_action_buttons();
-
-        // Add warning popup/noscript tag, if grades are changed by user.
-        $hasgrade = false;
-        if (!empty($this->_instance)) {
-            $hasgrade = $DB->record_exists_select('assign_grades',
-                                                  'assignment = ? AND grade <> -1',
-                                                  array($this->_instance));
-        }
-
-        if ($mform->elementExists('grade') && $hasgrade) {
-            $module = array(
-                'name' => 'mod_assign',
-                'fullpath' => '/mod/assign/module.js',
-                'requires' => array('node', 'event'),
-                'strings' => array(array('changegradewarning', 'mod_assign'))
-                );
-            $PAGE->requires->js_init_call('M.mod_assign.init_grade_change', null, false, $module);
-
-            // Add noscript tag in case.
-            $noscriptwarning = $mform->createElement('static',
-                                                     'warning',
-                                                     null,
-                                                     html_writer::tag('noscript',
-                                                     get_string('changegradewarning', 'mod_assign')));
-            $mform->insertElementBefore($noscriptwarning, 'grade');
-        }
     }
 
     /**
index 57c234b..5c92b96 100644 (file)
@@ -90,7 +90,7 @@ class mod_assign_base_testcase extends advanced_testcase {
 
         $this->resetAfterTest(true);
 
-        $this->course = $this->getDataGenerator()->create_course();
+        $this->course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
         $this->teachers = array();
         for ($i = 0; $i < self::DEFAULT_TEACHER_COUNT; $i++) {
             array_push($this->teachers, $this->getDataGenerator()->create_user());
@@ -350,4 +350,18 @@ class testable_assign extends assign {
 
         return $mform;
     }
+
+    public function testable_update_activity_completion_records($teamsubmission,
+                                                          $requireallteammemberssubmit,
+                                                          $submission,
+                                                          $userid,
+                                                          $complete,
+                                                          $completion) {
+        return parent::update_activity_completion_records($teamsubmission,
+                                                          $requireallteammemberssubmit,
+                                                          $submission,
+                                                          $userid,
+                                                          $complete,
+                                                          $completion);
+    }
 }
index 2062696..65670a5 100644 (file)
@@ -2631,4 +2631,94 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
         $grade = $assign->get_user_grade($this->students[0]->id, false);
         $this->assertEquals('30.0', $grade->grade);
     }
+
+    /**
+     * Test updating activity completion when submitting an assessment.
+     */
+    public function test_update_activity_completion_records_solitary_submission() {
+        $assign = $this->create_instance(array('grade' => 100,
+                'completion' => COMPLETION_TRACKING_AUTOMATIC,
+                'requireallteammemberssubmit' => 0));
+
+        $cm = $assign->get_course_module();
+
+        $student = $this->students[0];
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+
+        $this->setUser($student);
+
+        // Simulate a submission.
+        $data = new stdClass();
+        $data->onlinetext_editor = array(
+            'itemid' => file_get_unused_draft_itemid(),
+            'text' => 'Student submission text',
+            'format' => FORMAT_MOODLE
+        );
+        $completion = new completion_info($this->course);
+
+        $notices = array();
+        $assign->save_submission($data, $notices);
+
+        $submission = $assign->get_user_submission($student->id, true);
+
+        // Check that completion is not met yet.
+        $completiondata = $completion->get_data($cm, false, $student->id);
+        $this->assertEquals(0, $completiondata->completionstate);
+        $assign->testable_update_activity_completion_records(0, 0, $submission,
+                $student->id, COMPLETION_COMPLETE, $completion);
+        // Completion should now be met.
+        $completiondata = $completion->get_data($cm, false, $student->id);
+        $this->assertEquals(1, $completiondata->completionstate);
+    }
+
+    /**
+     * Test updating activity completion when submitting an assessment.
+     */
+    public function test_update_activity_completion_records_team_submission() {
+        $assign = $this->create_instance(array('grade' => 100,
+                'completion' => COMPLETION_TRACKING_AUTOMATIC,
+                 'teamsubmission' => 1));
+
+        $cm = $assign->get_course_module();
+
+        $student1 = $this->students[0];
+        $student2 = $this->students[1];
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+
+        // Put both users into a group.
+        $group1 = $this->getDataGenerator()->create_group(array('courseid' => $this->course->id));
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $student1->id));
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $student2->id));
+
+        $this->setUser($student1);
+
+        // Simulate a submission.
+        $data = new stdClass();
+        $data->onlinetext_editor = array(
+            'itemid' => file_get_unused_draft_itemid(),
+            'text' => 'Student submission text',
+            'format' => FORMAT_MOODLE
+        );
+        $completion = new completion_info($this->course);
+
+        $notices = array();
+        $assign->save_submission($data, $notices);
+
+        $submission = $assign->get_user_submission($student1->id, true);
+        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
+        $submission->groupid = $group1->id;
+
+        // Check that completion is not met yet.
+        $completiondata = $completion->get_data($cm, false, $student1->id);
+        $this->assertEquals(0, $completiondata->completionstate);
+        $completiondata = $completion->get_data($cm, false, $student2->id);
+        $this->assertEquals(0, $completiondata->completionstate);
+        $assign->testable_update_activity_completion_records(1, 0, $submission, $student1->id,
+                COMPLETION_COMPLETE, $completion);
+        // Completion should now be met.
+        $completiondata = $completion->get_data($cm, false, $student1->id);
+        $this->assertEquals(1, $completiondata->completionstate);
+        $completiondata = $completion->get_data($cm, false, $student2->id);
+        $this->assertEquals(1, $completiondata->completionstate);
+    }
 }
diff --git a/mod/data/classes/search/activity.php b/mod/data/classes/search/activity.php
new file mode 100644 (file)
index 0000000..eb776c3
--- /dev/null
@@ -0,0 +1,37 @@
+<?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/>.
+
+/**
+ * Search area for mod_data activities.
+ *
+ * @package    mod_data
+ * @copyright  2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace mod_data\search;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Search area for mod_data activities.
+ *
+ * @package    mod_data
+ * @copyright  2016 David Monllao {@link http://www.davidmonllao.com}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class activity extends \core_search\area\base_activity {
+}
index 0d01c13..a8e0d38 100644 (file)
@@ -331,6 +331,7 @@ $string['savesettings'] = 'Save settings';
 $string['savesuccess'] = 'Saved successfully. Your preset will now be available across the site.';
 $string['savetemplate'] = 'Save template';
 $string['search'] = 'Search';
+$string['search:activity'] = 'Database - activity information';
 $string['selectedrequired'] = 'All selected required';
 $string['showall'] = 'Show all entries';
 $string['single'] = 'View single';
index fe08813..4816808 100644 (file)
@@ -3078,6 +3078,12 @@ function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=fa
 
     // String cache
     static $str;
+    // This is an extremely hacky way to ensure we only print the 'unread' anchor
+    // the first time we encounter an unread post on a page. Ideally this would
+    // be moved into the caller somehow, and be better testable. But at the time
+    // of dealing with this bug, this static workaround was the most surgical and
+    // it fits together with only printing th unread anchor id once on a given page.
+    static $firstunreadanchorprinted = false;
 
     $modcontext = context_module::instance($cm->id);
 
@@ -3293,7 +3299,11 @@ function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=fa
             $forumpostclass = ' read';
         } else {
             $forumpostclass = ' unread';
-            $output .= html_writer::tag('a', '', array('name'=>'unread'));
+            // If this is the first unread post printed then give it an anchor and id of unread.
+            if (!$firstunreadanchorprinted) {
+                $output .= html_writer::tag('a', '', array('id' => 'unread'));
+                $firstunreadanchorprinted = true;
+            }
         }
     } else {
         // ignore trackign status if not tracked or tracked param missing
index 75f7d8e..e8709c6 100644 (file)
@@ -2596,17 +2596,31 @@ function lti_load_type_from_cartridge($url, $type) {
         array(
             "title" => "lti_typename",
             "launch_url" => "lti_toolurl",
-            "description" => "lti_description"
+            "description" => "lti_description",
+            "icon" => "lti_icon",
+            "secure_icon" => "lti_secureicon"
         ),
         array(
-            "icon_url" => "lti_icon",
-            "secure_icon_url" => "lti_secureicon"
+            "icon_url" => "lti_extension_icon",
+            "secure_icon_url" => "lti_extension_secureicon"
         )
     );
     // If an activity name exists, unset the cartridge name so we don't override it.
     if (isset($type->lti_typename)) {
         unset($toolinfo['lti_typename']);
     }
+
+    // Always prefer cartridge core icons first, then, if none are found, look at the extension icons.
+    if (empty($toolinfo['lti_icon']) && !empty($toolinfo['lti_extension_icon'])) {
+        $toolinfo['lti_icon'] = $toolinfo['lti_extension_icon'];
+    }
+    unset($toolinfo['lti_extension_icon']);
+
+    if (empty($toolinfo['lti_secureicon']) && !empty($toolinfo['lti_extension_secureicon'])) {
+        $toolinfo['lti_secureicon'] = $toolinfo['lti_extension_secureicon'];
+    }
+    unset($toolinfo['lti_extension_secureicon']);
+
     foreach ($toolinfo as $property => $value) {
         $type->$property = $value;
     }
@@ -2626,17 +2640,31 @@ function lti_load_tool_from_cartridge($url, $lti) {
             "title" => "name",
             "launch_url" => "toolurl",
             "secure_launch_url" => "securetoolurl",
-            "description" => "intro"
+            "description" => "intro",
+            "icon" => "icon",
+            "secure_icon" => "secureicon"
         ),
         array(
-            "icon_url" => "icon",
-            "secure_icon_url" => "secureicon"
+            "icon_url" => "extension_icon",
+            "secure_icon_url" => "extension_secureicon"
         )
     );
     // If an activity name exists, unset the cartridge name so we don't override it.
     if (isset($lti->name)) {
         unset($toolinfo['name']);
     }
+
+    // Always prefer cartridge core icons first, then, if none are found, look at the extension icons.
+    if (empty($toolinfo['icon']) && !empty($toolinfo['extension_icon'])) {
+        $toolinfo['icon'] = $toolinfo['extension_icon'];
+    }
+    unset($toolinfo['extension_icon']);
+
+    if (empty($toolinfo['secureicon']) && !empty($toolinfo['extension_secureicon'])) {
+        $toolinfo['secureicon'] = $toolinfo['extension_secureicon'];
+    }
+    unset($toolinfo['extension_secureicon']);
+
     foreach ($toolinfo as $property => $value) {
         $lti->$property = $value;
     }
index cd021e3..f4b9175 100644 (file)
@@ -544,14 +544,14 @@ function quiz_cron() {
 }
 
 /**
- * @param int $quizid the quiz id.
+ * @param int|array $quizids A quiz ID, or an array of quiz IDs.
  * @param int $userid the userid.
  * @param string $status 'all', 'finished' or 'unfinished' to control
  * @param bool $includepreviews
  * @return an array of all the user's attempts at this quiz. Returns an empty
  *      array if there are none.
  */
-function quiz_get_user_attempts($quizid, $userid, $status = 'finished', $includepreviews = false) {
+function quiz_get_user_attempts($quizids, $userid, $status = 'finished', $includepreviews = false) {
     global $DB, $CFG;
     // TODO MDL-33071 it is very annoying to have to included all of locallib.php
     // just to get the quiz_attempt::FINISHED constants, but I will try to sort
@@ -578,15 +578,18 @@ function quiz_get_user_attempts($quizid, $userid, $status = 'finished', $include
             break;
     }
 
+    $quizids = (array) $quizids;
+    list($insql, $inparams) = $DB->get_in_or_equal($quizids, SQL_PARAMS_NAMED);
+    $params += $inparams;
+    $params['userid'] = $userid;
+
     $previewclause = '';
     if (!$includepreviews) {
         $previewclause = ' AND preview = 0';
     }
 
-    $params['quizid'] = $quizid;
-    $params['userid'] = $userid;
     return $DB->get_records_select('quiz_attempts',
-            'quiz = :quizid AND userid = :userid' . $previewclause . $statuscondition,
+            "quiz $insql AND userid = :userid" . $previewclause . $statuscondition,
             $params, 'attempt ASC');
 }
 
@@ -1465,6 +1468,20 @@ function quiz_print_overview($courses, &$htmlarray) {
         return;
     }
 
+    // Get the quizzes attempts.
+    $attemptsinfo = [];
+    $quizids = [];
+    foreach ($quizzes as $quiz) {
+        $quizids[] = $quiz->id;
+        $attemptsinfo[$quiz->id] = ['count' => 0, 'hasfinished' => false];
+    }
+    $attempts = quiz_get_user_attempts($quizids, $USER->id);
+    foreach ($attempts as $attempt) {
+        $attemptsinfo[$attempt->quiz]['count']++;
+        $attemptsinfo[$attempt->quiz]['hasfinished'] = true;
+    }
+    unset($attempts);
+
     // Fetch some language strings outside the main loop.
     $strquiz = get_string('modulename', 'quiz');
     $strnoattempts = get_string('noattempts', 'quiz');
@@ -1474,15 +1491,7 @@ function quiz_print_overview($courses, &$htmlarray) {
     $now = time();
     foreach ($quizzes as $quiz) {
         if ($quiz->timeclose >= $now && $quiz->timeopen < $now) {
-            // Give a link to the quiz, and the deadline.
-            $str = '<div class="quiz overview">' .
-                    '<div class="name">' . $strquiz . ': <a ' .
-                    ($quiz->visible ? '' : ' class="dimmed"') .
-                    ' href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' .
-                    $quiz->coursemodule . '">' .
-                    $quiz->name . '</a></div>';
-            $str .= '<div class="info">' . get_string('quizcloseson', 'quiz',
-                    userdate($quiz->timeclose)) . '</div>';
+            $str = '';
 
             // Now provide more information depending on the uers's role.
             $context = context_module::instance($quiz->coursemodule);
@@ -1490,30 +1499,48 @@ function quiz_print_overview($courses, &$htmlarray) {
                 // For teacher-like people, show a summary of the number of student attempts.
                 // The $quiz objects returned by get_all_instances_in_course have the necessary $cm
                 // fields set to make the following call work.
-                $str .= '<div class="info">' .
-                        quiz_num_attempt_summary($quiz, $quiz, true) . '</div>';
-            } else if (has_any_capability(array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),
-                    $context)) { // Student
+                $str .= '<div class="info">' . quiz_num_attempt_summary($quiz, $quiz, true) . '</div>';
+
+            } else if (has_any_capability(array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), $context)) { // Student
                 // For student-like people, tell them how many attempts they have made.
-                if (isset($USER->id) &&
-                        ($attempts = quiz_get_user_attempts($quiz->id, $USER->id))) {
-                    $numattempts = count($attempts);
-                    $str .= '<div class="info">' .
-                            get_string('numattemptsmade', 'quiz', $numattempts) . '</div>';
+
+                if (isset($USER->id)) {
+                    if ($attemptsinfo[$quiz->id]['hasfinished']) {
+                        // The student's last attempt is finished.
+                        continue;
+                    }
+
+                    if ($attemptsinfo[$quiz->id]['count'] > 0) {
+                        $str .= '<div class="info">' .
+                            get_string('numattemptsmade', 'quiz', $attemptsinfo[$quiz->id]['count']) . '</div>';
+                    } else {
+                        $str .= '<div class="info">' . $strnoattempts . '</div>';
+                    }
+
                 } else {
                     $str .= '<div class="info">' . $strnoattempts . '</div>';
                 }
+
             } else {
                 // For ayone else, there is no point listing this quiz, so stop processing.
                 continue;
             }
 
-            // Add the output for this quiz to the rest.
-            $str .= '</div>';
+            // Give a link to the quiz, and the deadline.
+            $html = '<div class="quiz overview">' .
+                    '<div class="name">' . $strquiz . ': <a ' .
+                    ($quiz->visible ? '' : ' class="dimmed"') .
+                    ' href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' .
+                    $quiz->coursemodule . '">' .
+                    $quiz->name . '</a></div>';
+            $html .= '<div class="info">' . get_string('quizcloseson', 'quiz',
+                    userdate($quiz->timeclose)) . '</div>';
+            $html .= $str;
+            $html .= '</div>';
             if (empty($htmlarray[$quiz->course]['quiz'])) {
-                $htmlarray[$quiz->course]['quiz'] = $str;
+                $htmlarray[$quiz->course]['quiz'] = $html;
             } else {
-                $htmlarray[$quiz->course]['quiz'] .= $str;
+                $htmlarray[$quiz->course]['quiz'] .= $html;
             }
         }
     }
index f01eec8..af8cacf 100644 (file)
@@ -302,7 +302,11 @@ class quiz_overview_report extends quiz_attempts_report {
      * @uses exit. This method never returns.
      */
     protected function finish_regrade($nexturl) {
-        redirect($nexturl, get_string('regradecomplete', 'quiz_overview'), null, \core\output\notification::NOTIFY_SUCCESS);
+        global $OUTPUT;
+        \core\notification::success(get_string('regradecomplete', 'quiz_overview'));
+        echo $OUTPUT->continue_button($nexturl);
+        echo $OUTPUT->footer();
+        die();
     }
 
     /**
index 75edda7..22353ff 100644 (file)
@@ -227,4 +227,226 @@ class mod_quiz_lib_testcase extends advanced_testcase {
         $this->assertTrue(quiz_get_completion_state($course, $cm, $passstudent->id, 'return'));
         $this->assertFalse(quiz_get_completion_state($course, $cm, $failstudent->id, 'return'));
     }
+
+    public function test_quiz_get_user_attempts() {
+        global $DB;
+        $this->resetAfterTest();
+
+        $dg = $this->getDataGenerator();
+        $quizgen = $dg->get_plugin_generator('mod_quiz');
+        $course = $dg->create_course();
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+        $u3 = $dg->create_user();
+        $u4 = $dg->create_user();
+        $role = $DB->get_record('role', ['shortname' => 'student']);
+
+        $dg->enrol_user($u1->id, $course->id, $role->id);
+        $dg->enrol_user($u2->id, $course->id, $role->id);
+        $dg->enrol_user($u3->id, $course->id, $role->id);
+        $dg->enrol_user($u4->id, $course->id, $role->id);
+
+        $quiz1 = $quizgen->create_instance(['course' => $course->id, 'sumgrades' => 2]);
+        $quiz2 = $quizgen->create_instance(['course' => $course->id, 'sumgrades' => 2]);
+
+        // Questions.
+        $questgen = $dg->get_plugin_generator('core_question');
+        $quizcat = $questgen->create_question_category();
+        $question = $questgen->create_question('numerical', null, ['category' => $quizcat->id]);
+        quiz_add_quiz_question($question->id, $quiz1);
+        quiz_add_quiz_question($question->id, $quiz2);
+
+        $quizobj1a = quiz::create($quiz1->id, $u1->id);
+        $quizobj1b = quiz::create($quiz1->id, $u2->id);
+        $quizobj1c = quiz::create($quiz1->id, $u3->id);
+        $quizobj1d = quiz::create($quiz1->id, $u4->id);
+        $quizobj2a = quiz::create($quiz2->id, $u1->id);
+
+        // Set attempts.
+        $quba1a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1a->get_context());
+        $quba1a->set_preferred_behaviour($quizobj1a->get_quiz()->preferredbehaviour);
+        $quba1b = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1b->get_context());
+        $quba1b->set_preferred_behaviour($quizobj1b->get_quiz()->preferredbehaviour);
+        $quba1c = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1c->get_context());
+        $quba1c->set_preferred_behaviour($quizobj1c->get_quiz()->preferredbehaviour);
+        $quba1d = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1d->get_context());
+        $quba1d->set_preferred_behaviour($quizobj1d->get_quiz()->preferredbehaviour);
+        $quba2a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
+        $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
+
+        $timenow = time();
+
+        // User 1 passes quiz 1.
+        $attempt = quiz_create_attempt($quizobj1a, 1, false, $timenow, false, $u1->id);
+        quiz_start_new_attempt($quizobj1a, $quba1a, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj1a, $quba1a, $attempt);
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $attemptobj->process_submitted_actions($timenow, false, [1 => ['answer' => '3.14']]);
+        $attemptobj->process_finish($timenow, false);
+
+        // User 2 goes overdue in quiz 1.
+        $attempt = quiz_create_attempt($quizobj1b, 1, false, $timenow, false, $u2->id);
+        quiz_start_new_attempt($quizobj1b, $quba1b, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj1b, $quba1b, $attempt);
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $attemptobj->process_going_overdue($timenow, true);
+
+        // User 3 does not finish quiz 1.
+        $attempt = quiz_create_attempt($quizobj1c, 1, false, $timenow, false, $u3->id);
+        quiz_start_new_attempt($quizobj1c, $quba1c, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj1c, $quba1c, $attempt);
+
+        // User 4 abandons the quiz 1.
+        $attempt = quiz_create_attempt($quizobj1d, 1, false, $timenow, false, $u4->id);
+        quiz_start_new_attempt($quizobj1d, $quba1d, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj1d, $quba1d, $attempt);
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $attemptobj->process_abandon($timenow, true);
+
+        // User 1 attempts the quiz three times (abandon, finish, in progress).
+        $quba2a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
+        $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
+
+        $attempt = quiz_create_attempt($quizobj2a, 1, false, $timenow, false, $u1->id);
+        quiz_start_new_attempt($quizobj2a, $quba2a, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj2a, $quba2a, $attempt);
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $attemptobj->process_abandon($timenow, true);
+
+        $quba2a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
+        $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
+
+        $attempt = quiz_create_attempt($quizobj2a, 2, false, $timenow, false, $u1->id);
+        quiz_start_new_attempt($quizobj2a, $quba2a, $attempt, 2, $timenow);
+        quiz_attempt_save_started($quizobj2a, $quba2a, $attempt);
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $attemptobj->process_finish($timenow, false);
+
+        $quba2a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
+        $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
+
+        $attempt = quiz_create_attempt($quizobj2a, 3, false, $timenow, false, $u1->id);
+        quiz_start_new_attempt($quizobj2a, $quba2a, $attempt, 3, $timenow);
+        quiz_attempt_save_started($quizobj2a, $quba2a, $attempt);
+
+        // Check for user 1.
+        $attempts = quiz_get_user_attempts($quiz1->id, $u1->id, 'all');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u1->id, 'finished');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u1->id, 'unfinished');
+        $this->assertCount(0, $attempts);
+
+        // Check for user 2.
+        $attempts = quiz_get_user_attempts($quiz1->id, $u2->id, 'all');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::OVERDUE, $attempt->state);
+        $this->assertEquals($u2->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u2->id, 'finished');
+        $this->assertCount(0, $attempts);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u2->id, 'unfinished');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::OVERDUE, $attempt->state);
+        $this->assertEquals($u2->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        // Check for user 3.
+        $attempts = quiz_get_user_attempts($quiz1->id, $u3->id, 'all');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
+        $this->assertEquals($u3->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u3->id, 'finished');
+        $this->assertCount(0, $attempts);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u3->id, 'unfinished');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
+        $this->assertEquals($u3->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        // Check for user 4.
+        $attempts = quiz_get_user_attempts($quiz1->id, $u4->id, 'all');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
+        $this->assertEquals($u4->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u4->id, 'finished');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
+        $this->assertEquals($u4->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u4->id, 'unfinished');
+        $this->assertCount(0, $attempts);
+
+        // Multiple attempts for user 1 in quiz 2.
+        $attempts = quiz_get_user_attempts($quiz2->id, $u1->id, 'all');
+        $this->assertCount(3, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz2->id, $attempt->quiz);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz2->id, $attempt->quiz);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz2->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz2->id, $u1->id, 'finished');
+        $this->assertCount(2, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
+
+        $attempts = quiz_get_user_attempts($quiz2->id, $u1->id, 'unfinished');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+
+        // Multiple quiz attempts fetched at once.
+        $attempts = quiz_get_user_attempts([$quiz1->id, $quiz2->id], $u1->id, 'all');
+        $this->assertCount(4, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz2->id, $attempt->quiz);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz2->id, $attempt->quiz);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz2->id, $attempt->quiz);
+    }
+
 }
index 3553f45..422b6e6 100644 (file)
@@ -60,9 +60,9 @@ class mod_wiki_create_form extends moodleform {
                 }
                 $mform->addElement('radio', 'pageformat', '', get_string('format'.$format, 'wiki'), $format, $attr);
             }
+            $mform->addRule('pageformat', get_string('required'), 'required', null, 'client');
         }
         $mform->setType('pageformat', PARAM_ALPHANUMEXT);
-        $mform->addRule('pageformat', get_string('required'), 'required', null, 'client');
 
         if (!empty($this->_customdata['groups']->availablegroups)) {
             foreach ($this->_customdata['groups']->availablegroups as $groupdata) {
index c2194e3..6411bc5 100644 (file)
@@ -6,6 +6,16 @@
       "from": "abbrev@>=1.0.0 <2.0.0",
       "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.7.tgz"
     },
+    "acorn": {
+      "version": "3.2.0",
+      "from": "acorn@>=3.1.0 <4.0.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.2.0.tgz"
+    },
+    "acorn-jsx": {
+      "version": "3.0.1",
+      "from": "acorn-jsx@>=3.0.0 <4.0.0",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz"
+    },
     "align-text": {
       "version": "0.1.3",
       "from": "align-text@>=0.1.0 <0.2.0",
       "from": "ansi-color@*",
       "resolved": "https://registry.npmjs.org/ansi-color/-/ansi-color-0.2.1.tgz"
     },
+    "ansi-escapes": {
+      "version": "1.4.0",
+      "from": "ansi-escapes@>=1.1.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz"
+    },
     "ansi-regex": {
       "version": "2.0.0",
       "from": "ansi-regex@>=2.0.0 <3.0.0",
         }
       }
     },
+    "array-union": {
+      "version": "1.0.1",
+      "from": "array-union@>=1.0.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.1.tgz"
+    },
+    "array-uniq": {
+      "version": "1.0.2",
+      "from": "array-uniq@>=1.0.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.2.tgz"
+    },
+    "arrify": {
+      "version": "1.0.1",
+      "from": "arrify@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz"
+    },
     "asap": {
       "version": "1.0.0",
       "from": "asap@>=1.0.0 <1.1.0",
         }
       }
     },
+    "bluebird": {
+      "version": "3.4.0",
+      "from": "bluebird@>=3.1.1 <4.0.0",
+      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.0.tgz"
+    },
     "boom": {
       "version": "2.10.1",
       "from": "boom@>=2.0.0 <3.0.0",
       "from": "builtin-modules@>=1.0.0 <2.0.0",
       "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz"
     },
+    "caller-path": {
+      "version": "0.1.0",
+      "from": "caller-path@>=0.1.0 <0.2.0",
+      "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz"
+    },
+    "callsites": {
+      "version": "0.2.0",
+      "from": "callsites@>=0.2.0 <0.3.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz"
+    },
     "camelcase": {
       "version": "2.0.1",
       "from": "camelcase@>=2.0.0 <3.0.0",
         }
       }
     },
+    "cli-cursor": {
+      "version": "1.0.2",
+      "from": "cli-cursor@>=1.0.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz"
+    },
+    "cli-width": {
+      "version": "2.1.0",
+      "from": "cli-width@>=2.0.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.1.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"
     },
+    "code-point-at": {
+      "version": "1.0.0",
+      "from": "code-point-at@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.0.0.tgz"
+    },
     "coffee-script": {
       "version": "1.3.3",
       "from": "coffee-script@>=1.3.3 <1.4.0",
       "from": "cssproc@>=0.0.1 <0.1.0",
       "resolved": "https://registry.npmjs.org/cssproc/-/cssproc-0.0.7.tgz"
     },
+    "d": {
+      "version": "0.1.1",
+      "from": "d@>=0.1.1 <0.2.0",
+      "resolved": "https://registry.npmjs.org/d/-/d-0.1.1.tgz"
+    },
     "dashdash": {
       "version": "1.12.1",
       "from": "dashdash@>=1.10.1 <2.0.0",
       "from": "decamelize@>=1.1.2 <2.0.0",
       "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.1.2.tgz"
     },
+    "deep-is": {
+      "version": "0.1.3",
+      "from": "deep-is@>=0.1.3 <0.2.0",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz"
+    },
+    "del": {
+      "version": "2.2.0",
+      "from": "del@>=2.0.2 <3.0.0",
+      "resolved": "https://registry.npmjs.org/del/-/del-2.2.0.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"
     },
+    "doctrine": {
+      "version": "1.2.2",
+      "from": "doctrine@>=1.2.2 <2.0.0",
+      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.2.2.tgz",
+      "dependencies": {
+        "esutils": {
+          "version": "1.1.6",
+          "from": "esutils@>=1.1.6 <2.0.0",
+          "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz"
+        },
+        "isarray": {
+          "version": "1.0.0",
+          "from": "isarray@>=1.0.0 <2.0.0",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
+        }
+      }
+    },
     "dom-serializer": {
       "version": "0.1.0",
       "from": "dom-serializer@>=0.0.0 <1.0.0",
       "from": "error-ex@>=1.2.0 <2.0.0",
       "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.0.tgz"
     },
+    "es5-ext": {
+      "version": "0.10.11",
+      "from": "es5-ext@>=0.10.8 <0.11.0",
+      "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.11.tgz"
+    },
+    "es6-iterator": {
+      "version": "2.0.0",
+      "from": "es6-iterator@>=2.0.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.0.tgz"
+    },
+    "es6-map": {
+      "version": "0.1.4",
+      "from": "es6-map@>=0.1.3 <0.2.0",
+      "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.4.tgz",
+      "dependencies": {
+        "es6-symbol": {
+          "version": "3.1.0",
+          "from": "es6-symbol@>=3.1.0 <3.2.0",
+          "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.0.tgz"
+        }
+      }
+    },
+    "es6-set": {
+      "version": "0.1.4",
+      "from": "es6-set@>=0.1.3 <0.2.0",
+      "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.4.tgz"
+    },
+    "es6-symbol": {
+      "version": "3.0.2",
+      "from": "es6-symbol@>=3.0.1 <3.1.0",
+      "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.0.2.tgz"
+    },
+    "es6-weak-map": {
+      "version": "2.0.1",
+      "from": "es6-weak-map@>=2.0.1 <3.0.0",
+      "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.1.tgz"
+    },
     "escape-string-regexp": {
       "version": "1.0.4",
       "from": "escape-string-regexp@>=1.0.2 <2.0.0",
       "from": "escodegen@>=0.0.0 <0.1.0",
       "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-0.0.28.tgz"
     },
+    "escope": {
+      "version": "3.6.0",
+      "from": "escope@>=3.6.0 <4.0.0",
+      "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz",
+      "dependencies": {
+        "estraverse": {
+          "version": "4.2.0",
+          "from": "estraverse@4.2.0",
+          "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz"
+        }
+      }
+    },
+    "eslint": {
+      "version": "2.12.0",
+      "from": "eslint@>=2.0.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-2.12.0.tgz",
+      "dependencies": {
+        "ansi-styles": {
+          "version": "2.2.1",
+          "from": "ansi-styles@>=2.2.1 <3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz"
+        },
+        "argparse": {
+          "version": "1.0.7",
+          "from": "argparse@>=1.0.7 <2.0.0",
+          "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.7.tgz"
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "from": "chalk@>=1.1.3 <2.0.0",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz"
+        },
+        "debug": {
+          "version": "2.2.0",
+          "from": "debug@>=2.1.1 <3.0.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz"
+        },
+        "esprima": {
+          "version": "2.7.2",
+          "from": "esprima@>=2.6.0 <3.0.0",
+          "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.2.tgz"
+        },
+        "estraverse": {
+          "version": "4.2.0",
+          "from": "estraverse@>=4.2.0 <5.0.0",
+          "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz"
+        },
+        "glob": {
+          "version": "7.0.3",
+          "from": "glob@>=7.0.3 <8.0.0",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.3.tgz"
+        },
+        "js-yaml": {
+          "version": "3.6.1",
+          "from": "js-yaml@>=3.5.1 <4.0.0",
+          "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.1.tgz"
+        },
+        "lodash": {
+          "version": "4.13.1",
+          "from": "lodash@>=4.0.0 <5.0.0",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.13.1.tgz"
+        },
+        "minimatch": {
+          "version": "3.0.0",
+          "from": "minimatch@3.0.0",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz"
+        },
+        "progress": {
+          "version": "1.1.8",
+          "from": "progress@>=1.1.8 <2.0.0",
+          "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz"
+        },
+        "shelljs": {
+          "version": "0.6.0",
+          "from": "shelljs@>=0.6.0 <0.7.0",
+          "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.6.0.tgz"
+        }
+      }
+    },
+    "espree": {
+      "version": "3.1.4",
+      "from": "espree@3.1.4",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-3.1.4.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"
     },
+    "esrecurse": {
+      "version": "4.1.0",
+      "from": "esrecurse@>=4.1.0 <5.0.0",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.1.0.tgz",
+      "dependencies": {
+        "estraverse": {
+          "version": "4.1.1",
+          "from": "estraverse@>=4.1.0 <4.2.0",
+          "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.1.1.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"
     },
+    "esutils": {
+      "version": "2.0.2",
+      "from": "esutils@>=2.0.2 <3.0.0",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz"
+    },
+    "event-emitter": {
+      "version": "0.3.4",
+      "from": "event-emitter@>=0.3.4 <0.4.0",
+      "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.4.tgz"
+    },
     "eventemitter2": {
       "version": "0.4.14",
       "from": "eventemitter2@>=0.4.13 <0.5.0",
       "from": "exit@>=0.1.1 <0.2.0",
       "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz"
     },
+    "exit-hook": {
+      "version": "1.1.1",
+      "from": "exit-hook@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz"
+    },
     "extend": {
       "version": "3.0.0",
       "from": "extend@>=3.0.0 <3.1.0",
       "from": "extsprintf@1.0.2",
       "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz"
     },
+    "fast-levenshtein": {
+      "version": "1.1.3",
+      "from": "fast-levenshtein@>=1.1.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.3.tgz"
+    },
     "faye-websocket": {
       "version": "0.4.4",
       "from": "faye-websocket@>=0.4.3 <0.5.0",
       "from": "figures@>=1.0.1 <2.0.0",
       "resolved": "https://registry.npmjs.org/figures/-/figures-1.4.0.tgz"
     },
+    "file-entry-cache": {
+      "version": "1.2.4",
+      "from": "file-entry-cache@>=1.1.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-1.2.4.tgz"
+    },
     "fileset": {
       "version": "0.1.8",
       "from": "fileset@>=0.1.0 <0.2.0",
         }
       }
     },
+    "flat-cache": {
+      "version": "1.0.10",
+      "from": "flat-cache@>=1.0.9 <2.0.0",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.0.10.tgz",
+      "dependencies": {
+        "graceful-fs": {
+          "version": "4.1.4",
+          "from": "graceful-fs@>=4.1.2 <5.0.0",
+          "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.4.tgz"
+        }
+      }
+    },
     "forever-agent": {
       "version": "0.6.1",
       "from": "forever-agent@>=0.6.1 <0.7.0",
         }
       }
     },
+    "globals": {
+      "version": "9.8.0",
+      "from": "globals@>=9.2.0 <10.0.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-9.8.0.tgz"
+    },
+    "globby": {
+      "version": "4.1.0",
+      "from": "globby@>=4.0.0 <5.0.0",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-4.1.0.tgz",
+      "dependencies": {
+        "glob": {
+          "version": "6.0.4",
+          "from": "glob@>=6.0.1 <7.0.0",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz"
+        },
+        "minimatch": {
+          "version": "3.0.0",
+          "from": "minimatch@>=2.0.0 <3.0.0||>=3.0.0 <4.0.0",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz"
+        }
+      }
+    },
     "globule": {
       "version": "0.1.0",
       "from": "globule@>=0.1.0 <0.2.0",
         }
       }
     },
+    "grunt-eslint": {
+      "version": "18.1.0",
+      "from": "grunt-eslint@>=18.1.0 <19.0.0",
+      "resolved": "https://registry.npmjs.org/grunt-eslint/-/grunt-eslint-18.1.0.tgz"
+    },
     "grunt-legacy-log": {
       "version": "0.1.3",
       "from": "grunt-legacy-log@>=0.1.0 <0.2.0",
       "from": "iconv-lite@>=0.2.11 <0.3.0",
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz"
     },
+    "ignore": {
+      "version": "3.1.2",
+      "from": "ignore@>=3.1.2 <4.0.0",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.1.2.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"
     },
+    "imurmurhash": {
+      "version": "0.1.4",
+      "from": "imurmurhash@>=0.1.4 <0.2.0",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz"
+    },
     "indent-string": {
       "version": "2.1.0",
       "from": "indent-string@>=2.1.0 <3.0.0",
       "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz"
     },
+    "inflight": {
+      "version": "1.0.5",
+      "from": "inflight@>=1.0.4 <2.0.0",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz"
+    },
     "inherits": {
       "version": "2.0.1",
       "from": "inherits@>=2.0.0 <3.0.0",
       "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
     },
+    "inquirer": {
+      "version": "0.12.0",
+      "from": "inquirer@>=0.12.0 <0.13.0",
+      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz",
+      "dependencies": {
+        "lodash": {
+          "version": "4.13.1",
+          "from": "lodash@>=4.3.0 <5.0.0"
+        }
+      }
+    },
     "is-arrayish": {
       "version": "0.2.1",
       "from": "is-arrayish@>=0.2.1 <0.3.0",
       "from": "is-finite@>=1.0.0 <2.0.0",
       "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.1.tgz"
     },
+    "is-fullwidth-code-point": {
+      "version": "1.0.0",
+      "from": "is-fullwidth-code-point@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.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"
     },
+    "is-path-cwd": {
+      "version": "1.0.0",
+      "from": "is-path-cwd@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz"
+    },
+    "is-path-in-cwd": {
+      "version": "1.0.0",
+      "from": "is-path-in-cwd@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz"
+    },
+    "is-path-inside": {
+      "version": "1.0.0",
+      "from": "is-path-inside@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.0.tgz"
+    },
     "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"
     },
+    "is-resolvable": {
+      "version": "1.0.0",
+      "from": "is-resolvable@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz"
+    },
     "is-typedarray": {
       "version": "1.0.0",
       "from": "is-typedarray@>=1.0.0 <1.1.0",
       "from": "json-schema@0.2.2",
       "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz"
     },
+    "json-stable-stringify": {
+      "version": "1.0.1",
+      "from": "json-stable-stringify@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.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"
     },
+    "jsonify": {
+      "version": "0.0.0",
+      "from": "jsonify@>=0.0.0 <0.1.0",
+      "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz"
+    },
     "jsonpointer": {
       "version": "2.0.0",
       "from": "jsonpointer@2.0.0",
         }
       }
     },
+    "levn": {
+      "version": "0.3.0",
+      "from": "levn@>=0.3.0 <0.4.0",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz"
+    },
     "load-json-file": {
       "version": "1.1.0",
       "from": "load-json-file@>=1.0.0 <2.0.0",
       "from": "mkdirp@>=0.5.0 <0.6.0",
       "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz"
     },
+    "ms": {
+      "version": "0.7.1",
+      "from": "ms@0.7.1",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
+    },
+    "mute-stream": {
+      "version": "0.0.5",
+      "from": "mute-stream@0.0.5",
+      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz"
+    },
     "node-uuid": {
       "version": "1.4.7",
       "from": "node-uuid@>=1.4.7 <1.5.0",
       "from": "object-assign@>=4.0.1 <5.0.0",
       "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.0.1.tgz"
     },
+    "once": {
+      "version": "1.3.3",
+      "from": "once@>=1.3.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz"
+    },
+    "onetime": {
+      "version": "1.1.0",
+      "from": "onetime@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz"
+    },
     "optimist": {
       "version": "0.3.7",
       "from": "optimist@>=0.3.0 <0.4.0",
       "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz"
     },
+    "optionator": {
+      "version": "0.8.1",
+      "from": "optionator@>=0.8.1 <0.9.0",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.1.tgz",
+      "dependencies": {
+        "wordwrap": {
+          "version": "1.0.0",
+          "from": "wordwrap@>=1.0.0 <1.1.0",
+          "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz"
+        }
+      }
+    },
+    "os-homedir": {
+      "version": "1.0.1",
+      "from": "os-homedir@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.1.tgz"
+    },
     "pako": {
       "version": "0.2.8",
       "from": "pako@>=0.2.0 <0.3.0",
       "from": "path-exists@>=2.0.0 <3.0.0",
       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz"
     },
+    "path-is-absolute": {
+      "version": "1.0.0",
+      "from": "path-is-absolute@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz"
+    },
+    "path-is-inside": {
+      "version": "1.0.1",
+      "from": "path-is-inside@>=1.0.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.1.tgz"
+    },
     "path-type": {
       "version": "1.1.0",
       "from": "path-type@>=1.0.0 <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"
     },
+    "pluralize": {
+      "version": "1.2.1",
+      "from": "pluralize@>=1.2.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz"
+    },
+    "prelude-ls": {
+      "version": "1.1.2",
+      "from": "prelude-ls@>=1.1.2 <1.2.0",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz"
+    },
     "pretty-bytes": {
       "version": "1.0.4",
       "from": "pretty-bytes@>=1.0.0 <2.0.0",
       "from": "qs@>=5.2.0 <5.3.0",
       "resolved": "https://registry.npmjs.org/qs/-/qs-5.2.0.tgz"
     },
+    "read-json-sync": {
+      "version": "1.1.1",
+      "from": "read-json-sync@>=1.1.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/read-json-sync/-/read-json-sync-1.1.1.tgz",
+      "dependencies": {
+        "graceful-fs": {
+          "version": "4.1.4",
+          "from": "graceful-fs@4.1.4",
+          "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.4.tgz"
+        }
+      }
+    },
     "read-pkg": {
       "version": "1.1.0",
       "from": "read-pkg@>=1.0.0 <2.0.0",
       "from": "readable-stream@>=1.1.0 <1.2.0",
       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.13.tgz"
     },
+    "readline2": {
+      "version": "1.0.1",
+      "from": "readline2@>=1.0.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz"
+    },
     "redent": {
       "version": "1.0.0",
       "from": "redent@>=1.0.0 <2.0.0",
       "from": "request@>=2.51.0 <3.0.0",
       "resolved": "https://registry.npmjs.org/request/-/request-2.67.0.tgz"
     },
+    "require-uncached": {
+      "version": "1.0.2",
+      "from": "require-uncached@>=1.0.2 <2.0.0",
+      "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.2.tgz"
+    },
     "resolve": {
       "version": "0.4.3",
       "from": "resolve@>=0.4.0 <0.5.0",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.4.3.tgz"
     },
+    "resolve-from": {
+      "version": "1.0.1",
+      "from": "resolve-from@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz"
+    },
+    "restore-cursor": {
+      "version": "1.0.1",
+      "from": "restore-cursor@>=1.0.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz"
+    },
     "right-align": {
       "version": "0.1.3",
       "from": "right-align@>=0.1.1 <0.2.0",
       "from": "rimraf@>=2.2.8 <2.3.0",
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz"
     },
+    "run-async": {
+      "version": "0.1.0",
+      "from": "run-async@>=0.1.0 <0.2.0",
+      "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz"
+    },
+    "rx-lite": {
+      "version": "3.1.2",
+      "from": "rx-lite@>=3.1.2 <4.0.0",
+      "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz"
+    },
     "sax": {
       "version": "0.5.8",
       "from": "sax@>=0.5.0 <0.6.0",
       "from": "signal-exit@>=2.1.2 <3.0.0",
       "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-2.1.2.tgz"
     },
+    "slice-ansi": {
+      "version": "0.0.4",
+      "from": "slice-ansi@0.0.4",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz"
+    },
     "sntp": {
       "version": "1.0.9",
       "from": "sntp@>=1.0.0 <2.0.0",
       "from": "spdx-license-ids@>=1.0.2 <2.0.0",
       "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.0.tgz"
     },
+    "sprintf-js": {
+      "version": "1.0.3",
+      "from": "sprintf-js@>=1.0.2 <1.1.0",
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz"
+    },
     "sshpk": {
       "version": "1.7.3",
       "from": "sshpk@>=1.7.0 <2.0.0",
       "from": "string_decoder@>=0.10.0 <0.11.0",
       "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
     },
+    "string-width": {
+      "version": "1.0.1",
+      "from": "string-width@>=1.0.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.1.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"
     },
     "strip-ansi": {
-      "version": "3.0.0",
+      "version": "3.0.1",
       "from": "strip-ansi@>=3.0.0 <4.0.0",
-      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.0.tgz"
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz"
     },
     "strip-bom": {
       "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"
     },
+    "table": {
+      "version": "3.7.8",
+      "from": "table@>=3.7.8 <4.0.0",
+      "resolved": "https://registry.npmjs.org/table/-/table-3.7.8.tgz",
+      "dependencies": {
+        "lodash": {
+          "version": "4.13.1",
+          "from": "lodash@>=4.0.0 <5.0.0"
+        }
+      }
+    },
+    "text-table": {
+      "version": "0.2.0",
+      "from": "text-table@>=0.2.0 <0.3.0",
+      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz"
+    },
+    "through": {
+      "version": "2.3.8",
+      "from": "through@>=2.3.6 <3.0.0",
+      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz"
+    },
     "timethat": {
       "version": "0.0.3",
       "from": "timethat@>=0.0.1 <0.1.0",
       "from": "trim-newlines@>=1.0.0 <2.0.0",
       "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz"
     },
+    "tryit": {
+      "version": "1.0.2",
+      "from": "tryit@>=1.0.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.2.tgz"
+    },
     "tunnel-agent": {
       "version": "0.4.2",
       "from": "tunnel-agent@>=0.4.1 <0.5.0",
       "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.2.tgz"
     },
+    "tv4": {
+      "version": "1.2.7",
+      "from": "tv4@>=1.2.7 <2.0.0",
+      "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.2.7.tgz"
+    },
     "tweetnacl": {
       "version": "0.13.3",
       "from": "tweetnacl@>=0.13.0 <1.0.0",
       "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.13.3.tgz"
     },
+    "type-check": {
+      "version": "0.3.2",
+      "from": "type-check@>=0.3.2 <0.4.0",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz"
+    },
     "typedarray": {
       "version": "0.0.6",
       "from": "typedarray@>=0.0.5 <0.1.0",
       "from": "uri-path@>=1.0.0 <2.0.0",
       "resolved": "https://registry.npmjs.org/uri-path/-/uri-path-1.0.0.tgz"
     },
+    "user-home": {
+      "version": "2.0.0",
+      "from": "user-home@>=2.0.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz"
+    },
     "util-deprecate": {
       "version": "1.0.2",
       "from": "util-deprecate@>=1.0.1 <1.1.0",
       "from": "wordwrap@0.0.2",
       "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz"
     },
+    "wrappy": {
+      "version": "1.0.2",
+      "from": "wrappy@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
+    },
+    "write": {
+      "version": "0.2.1",
+      "from": "write@>=0.2.1 <0.3.0",
+      "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz"
+    },
     "xml2js": {
       "version": "0.2.8",
       "from": "xml2js@>=0.2.0 <0.3.0",
       "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.8.tgz"
     },
+    "xmldom": {
+      "version": "0.1.22",
+      "from": "xmldom@latest",
+      "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.22.tgz"
+    },
+    "xpath": {
+      "version": "0.0.23",
+      "from": "xpath@latest",
+      "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.23.tgz"
+    },
+    "xregexp": {
+      "version": "3.1.1",
+      "from": "xregexp@>=3.0.0 <4.0.0",
+      "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-3.1.1.tgz"
+    },
     "xtend": {
       "version": "4.0.1",
       "from": "xtend@>=4.0.0 <5.0.0",
index 6c3be76..5989a0e 100644 (file)
@@ -1,14 +1,17 @@
 {
-    "name": "Moodle",
-    "private": true,
-    "description": "Moodle",
-    "devDependencies": {
-        "async": "^1.5.2",
-        "grunt": "0.4.5",
-        "grunt-contrib-jshint": "0.11.3",
-        "grunt-contrib-less": "1.1.0",
-        "grunt-contrib-uglify": "0.11.0",
-        "grunt-contrib-watch": "0.6.1",
-        "shifter": "0.5.0"
-    }
+  "name": "Moodle",
+  "private": true,
+  "description": "Moodle",
+  "devDependencies": {
+    "async": "^1.5.2",
+    "grunt": "0.4.5",
+    "grunt-contrib-jshint": "0.11.3",
+    "grunt-contrib-less": "1.1.0",
+    "grunt-contrib-uglify": "0.11.0",
+    "grunt-contrib-watch": "0.6.1",
+    "grunt-eslint": "^18.1.0",
+    "shifter": "0.5.0",
+    "xmldom": "^0.1.22",
+    "xpath": "0.0.23"
+  }
 }
diff --git a/pix/i/competencies.png b/pix/i/competencies.png
new file mode 100644 (file)
index 0000000..4b2f7d7
Binary files /dev/null and b/pix/i/competencies.png differ
diff --git a/pix/i/competencies.svg b/pix/i/competencies.svg
new file mode 100644 (file)
index 0000000..8023105
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>\r
+<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In  -->\r
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
+       <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
+]>\r
+<svg version="1.1"\r
+        xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"\r
+        x="0px" y="0px" width="16px" height="16px" viewBox="-0.042 -0.036 16 16"\r
+        style="overflow:visible;enable-background:new -0.042 -0.036 16 16;" xml:space="preserve">\r
+<defs>\r
+</defs>\r
+<path style="fill:#999999;" d="M15.821,15.071c0.248,0.491,0,0.893-0.55,0.893H0.646c-0.55,0-0.797-0.401-0.55-0.893l1.566-3.107\r
+       h12.592L15.821,15.071z M4.184,6.964l-2.017,4H13.75l-2.017-4H4.184z M11.23,5.964L8.409,0.369c-0.248-0.491-0.653-0.491-0.9,0\r
+       L4.688,5.964H11.23z"/>\r
+</svg>\r
index 3959382..081db48 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 
 require_once($CFG->libdir . '/portfoliolib.php');
+require_once($CFG->libdir . '/portfolio/plugin.php');
 
 class portfolio_plugin_download extends portfolio_plugin_pull_base {
 
index cb845ad..a2928b7 100644 (file)
@@ -43,8 +43,17 @@ class tagareacollection extends \core\output\inplace_editable {
      * @param \stdClass $tagarea
      */
     public function __construct($tagarea) {
+        if (!empty($tagarea->locked)) {
+            // If the tag collection for the current tag area is locked, display the
+            // name of the collection without possibility to edit it.
+            $tagcoll = \core_tag_collection::get_by_id($tagarea->tagcollid);
+            parent::__construct('core_tag', 'tagareacollection', $tagarea->id, false,
+                \core_tag_collection::display_name($tagcoll), $tagarea->tagcollid);
+            return;
+        }
+
         $tagcollections = \core_tag_collection::get_collections_menu(true);
-        $editable = (count($tagcollections) > 1) && empty($tagarea->locked) &&
+        $editable = (count($tagcollections) > 1) &&
                 has_capability('moodle/tag:manage', context_system::instance());
         $areaname = core_tag_area::display_name($tagarea->component, $tagarea->itemtype);
         $edithint = new lang_string('edittagcollection', 'core_tag');
index 26ee690..02d568b 100644 (file)
@@ -1297,7 +1297,7 @@ class core_tag_tag {
                 require_once($CFG->dirroot . '/' . ltrim($tagarea->callbackfile, '/'));
             }
             $callback = $tagarea->callback;
-            return $callback($this, $exclusivemode, $fromctx, $ctx, $rec, $page);
+            return call_user_func_array($callback, [$this, $exclusivemode, $fromctx, $ctx, $rec, $page]);
         }
         return null;
     }
index 788e3e8..c8bf079 100644 (file)
@@ -28,6 +28,7 @@ $choose = optional_param('choose', '', PARAM_PLUGIN);
 $reset  = optional_param('reset', 0, PARAM_BOOL);
 $device = optional_param('device', '', PARAM_TEXT);
 $unsettheme = optional_param('unsettheme', 0, PARAM_BOOL);
+$confirmation = optional_param('confirmation', 0, PARAM_BOOL);
 
 admin_externalpage_setup('themeselector');
 
@@ -44,6 +45,17 @@ unset($SESSION->theme);
 
 if ($reset and confirm_sesskey()) {
     theme_reset_all_caches();
+} else if ($choose && $confirmation) {
+
+    $theme = theme_config::load($choose);
+    echo $OUTPUT->header();
+    echo $OUTPUT->heading(get_string('themesaved'));
+    echo $OUTPUT->box_start();
+    echo format_text(get_string('choosereadme', 'theme_'.$theme->name), FORMAT_MOODLE);
+    echo $OUTPUT->box_end();
+    echo $OUTPUT->continue_button($CFG->wwwroot . '/theme/index.php');
+    echo $OUTPUT->footer();
+    exit;
 
 } else if ($choose && $device && !theme_is_device_locked($device) && !$unsettheme && confirm_sesskey()) {
     // Load the theme to make sure it is valid.
@@ -53,27 +65,8 @@ if ($reset and confirm_sesskey()) {
     $themename = core_useragent::get_device_type_cfg_var_name($device);
     set_config($themename, $theme->name);
 
-    // Create a new page for the display of the themes readme.
-    // This ensures that the readme page is shown using the new theme.
-    $confirmpage = new moodle_page();
-    $confirmpage->set_context($PAGE->context);
-    $confirmpage->set_url($PAGE->url);
-    $confirmpage->set_pagelayout($PAGE->pagelayout);
-    $confirmpage->set_pagetype($PAGE->pagetype);
-    $confirmpage->set_title($PAGE->title);
-    $confirmpage->set_heading($PAGE->heading);
-
-    // Get the core renderer for the new theme.
-    $output = $confirmpage->get_renderer('core');
-
-    echo $output->header();
-    echo $output->heading(get_string('themesaved'));
-    echo $output->box_start();
-    echo format_text(get_string('choosereadme', 'theme_'.$theme->name), FORMAT_MOODLE);
-    echo $output->box_end();
-    echo $output->continue_button($CFG->wwwroot . '/theme/index.php');
-    echo $output->footer();
-    exit;
+    $urlconfirm = new moodle_url('/theme/index.php', array('confirmation' => 1, 'choose' => $choose));
+    redirect($urlconfirm);
 } else if ($device && !theme_is_device_locked($device) && $unsettheme && confirm_sesskey() && ($device != 'default')) {
     // Unset the theme and continue.
     unset_config(core_useragent::get_device_type_cfg_var_name($device));
index 09a8aaf..5734d6b 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2016052300.02;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2016052300.03;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
-$release  = '3.2dev (Build: 20160603)'; // Human-friendly version name
+$release  = '3.2dev (Build: 20160609)'; // Human-friendly version name
 
 $branch   = '32';                       // This version's branch.
 $maturity = MATURITY_ALPHA;             // This version's maturity level.