--- /dev/null
+{
+ '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',
+ }
+}
/lib/yuilib/*/*/*-coverage.js
atlassian-ide-plugin.xml
/node_modules/
+.eslintignore
// 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.
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');
}
}
* @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
}]
}
},
},
yui: {
files: ['**/yui/src/**/*.js'],
- tasks: ['shifter']
+ tasks: ['yui']
},
},
shifter: {
}
});
+ /**
+ * 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.
* 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');
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 {
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');
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);
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']);
$dom->appendChild($top);
$top->appendChild($dom->createElement('shortname', $role->shortname));
- $top->appendChild($dom->createElement('name', $role->name));
- $top->appendChild($dom->createElement('description', $role->description));
+ $top->appendChild($dom->createElement('name', htmlspecialchars($role->name, ENT_COMPAT | ENT_HTML401, 'UTF-8')));
+ $top->appendChild($dom->createElement('description', htmlspecialchars($role->description, ENT_COMPAT | ENT_HTML401,
+ 'UTF-8')));
$top->appendChild($dom->createElement('archetype', $role->archetype));
$contextlevels = $dom->createElement('contextlevels');
$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');
try {
config = JSON.parse(self._competency.ruleconfig);
} catch (e) {
+ // eslint-disable-line no-empty
}
}
UserCompetencyCourseNavigation.prototype._courseId = null;
/** @type {String} Plugin base url. */
UserCompetencyCourseNavigation.prototype._baseUrl = null;
- /** @type {Boolean} Ignore the first change event for users. */
- UserCompetencyCourseNavigation.prototype._ignoreFirstUser = null;
/** @type {Boolean} Ignore the first change event for competencies. */
UserCompetencyCourseNavigation.prototype._ignoreFirstCompetency = null;
navigation_node::TYPE_SETTING,
null,
null,
- new pix_icon('competency', '', 'tool_lp'));
+ new pix_icon('i/competencies', ''));
if (isset($settingsnode)) {
$navigation->add_node($settingsnode);
}
navigation_node::TYPE_SETTING,
null,
null,
- new pix_icon('competency', '', 'tool_lp'));
+ new pix_icon('i/competencies', ''));
if (isset($settingsnode)) {
$navigation->add_node($settingsnode);
}
navigation_node::TYPE_SETTING,
null,
null,
- new pix_icon('competency', '', 'tool_lp'));
+ new pix_icon('i/competencies', ''));
if (isset($settingsnode)) {
$navigation->add_node($settingsnode);
}
+++ /dev/null
-<?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>
<dt>{{#str}}rating, tool_lp{{/str}}</dt>
<dd>{{gradename}}
{{#cangrade}}
- <buttn class="btn" id="rate_{{uniqid}}">{{#str}}rate, tool_lp{{/str}}</button>
+ <button class="btn" id="rate_{{uniqid}}">{{#str}}rate, tool_lp{{/str}}</button>
{{/cangrade}}
</dd>
{{#js}}
<dt>{{#str}}rating, tool_lp{{/str}}</dt>
<dd>{{gradename}}
{{#cangrade}}
- <buttn class="btn" id="rate_{{uniqid}}">{{#str}}rate, tool_lp{{/str}}</button>
+ <button class="btn" id="rate_{{uniqid}}">{{#str}}rate, tool_lp{{/str}}</button>
{{/cangrade}}
</dd>
{{/usercompetencycourse}}
--- /dev/null
+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.
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');
}
+++ /dev/null
-<?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.'&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.'&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();
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');
}
$grade_setting = new backup_nested_element('grade_setting', 'id', array(
'name', 'value'));
+ $gradebook_attributes = new backup_nested_element('attributes', null, array('calculations_freeze'));
// Build the tree
+ $gradebook->add_child($gradebook_attributes);
+
$gradebook->add_child($grade_categories);
$grade_categories->add_child($grade_category);
$gradebook->add_child($grade_settings);
$grade_settings->add_child($grade_setting);
+ // Define sources
+
// Add attribute with gradebook calculation freeze date if needed.
+ $attributes = new stdClass();
$gradebookcalculationfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->get_courseid());
if ($gradebookcalculationfreeze) {
- $gradebook->add_attributes(array('calculations_freeze'));
- $gradebook->get_attribute('calculations_freeze')->set_value($gradebookcalculationfreeze);
+ $attributes->calculations_freeze = $gradebookcalculationfreeze;
}
-
- // Define sources
+ $gradebook_attributes->set_source_array([$attributes]);
//Include manual, category and the course grade item
$grade_items_sql ="SELECT * FROM {grade_items}
$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];
return false;
}
+ // Identify the backup we're dealing with.
+ $backuprelease = floatval($this->get_task()->get_info()->backup_release); // The major version: 2.9, 3.0, ...
+ $backupbuild = 0;
+ preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
+ if (!empty($matches[1])) {
+ $backupbuild = (int) $matches[1]; // The date of Moodle build at the time of the backup.
+ }
+
+ // On older versions the freeze value has to be converted.
+ // We do this from here as it is happening right before the file is read.
+ // This only targets the backup files that can contain the legacy freeze.
+ if ($backupbuild > 20150618 && ($backuprelease < 3.0 || $backupbuild < 20160527)) {
+ $this->rewrite_step_backup_file_for_legacy_freeze($fullpath);
+ }
+
// Arrived here, execute the step
return true;
}
$paths = array();
$userinfo = $this->task->get_setting_value('users');
- $paths[] = new restore_path_element('gradebook', '/gradebook');
+ $paths[] = new restore_path_element('attributes', '/gradebook/attributes');
$paths[] = new restore_path_element('grade_category', '/gradebook/grade_categories/grade_category');
$paths[] = new restore_path_element('grade_item', '/gradebook/grade_items/grade_item');
if ($userinfo) {
return $paths;
}
- protected function process_gradebook($data) {
+ protected function process_attributes($data) {
// For non-merge restore types:
// Unset 'gradebook_calculations_freeze_' in the course and replace with the one from the backup.
$target = $this->get_task()->get_target();
}
}
}
+
+ /**
+ * Rewrite step definition to handle the legacy freeze attribute.
+ *
+ * In previous backups the calculations_freeze property was stored as an attribute of the
+ * top level node <gradebook>. The backup API, however, do not process grandparent nodes.
+ * It only processes definitive children, and their parent attributes.
+ *
+ * We had:
+ *
+ * <gradebook calculations_freeze="20160511">
+ * <grade_categories>
+ * <grade_category id="10">
+ * <depth>1</depth>
+ * ...
+ * </grade_category>
+ * </grade_categories>
+ * ...
+ * </gradebook>
+ *
+ * And this method will convert it to:
+ *
+ * <gradebook >
+ * <attributes>
+ * <calculations_freeze>20160511</calculations_freeze>
+ * </attributes>
+ * <grade_categories>
+ * <grade_category id="10">
+ * <depth>1</depth>
+ * ...
+ * </grade_category>
+ * </grade_categories>
+ * ...
+ * </gradebook>
+ *
+ * Note that we cannot just load the XML file in memory as it could potentially be huge.
+ * We can also completely ignore if the node <attributes> is already in the backup
+ * file as it never existed before.
+ *
+ * @param string $filepath The absolute path to the XML file.
+ * @return void
+ */
+ protected function rewrite_step_backup_file_for_legacy_freeze($filepath) {
+ $foundnode = false;
+ $newfile = make_request_directory(true) . DIRECTORY_SEPARATOR . 'file.xml';
+ $fr = fopen($filepath, 'r');
+ $fw = fopen($newfile, 'w');
+ if ($fr && $fw) {
+ while (($line = fgets($fr, 4096)) !== false) {
+ if (!$foundnode && strpos($line, '<gradebook ') === 0) {
+ $foundnode = true;
+ $matches = array();
+ $pattern = '@calculations_freeze=.([0-9]+).@';
+ if (preg_match($pattern, $line, $matches)) {
+ $freeze = $matches[1];
+ $line = preg_replace($pattern, '', $line);
+ $line .= " <attributes>\n <calculations_freeze>$freeze</calculations_freeze>\n </attributes>\n";
+ }
+ }
+ fputs($fw, $line);
+ }
+ if (!feof($fr)) {
+ throw new restore_step_exception('Error while attempting to rewrite the gradebook step file.');
+ }
+ fclose($fr);
+ fclose($fw);
+ if (!rename($newfile, $filepath)) {
+ throw new restore_step_exception('Error while attempting to rename the gradebook step file.');
+ }
+ } else {
+ if ($fr) {
+ fclose($fr);
+ }
+ if ($fw) {
+ fclose($fw);
+ }
+ }
+ }
+
}
/**
--- /dev/null
+<gradebook >
+ <attributes>
+ <calculations_freeze>20160511</calculations_freeze>
+ </attributes>
+ <grade_categories>
+ <grade_category id="10">
+ <depth>1</depth>
+ </grade_category>
+ </grade_categories>
+</gradebook>
--- /dev/null
+<gradebook calculations_freeze="20160511">
+ <grade_categories>
+ <grade_category id="10">
+ <depth>1</depth>
+ </grade_category>
+ </grade_categories>
+</gradebook>
--- /dev/null
+<gradebook some_other_value="false" >
+ <attributes>
+ <calculations_freeze>20160511</calculations_freeze>
+ </attributes>
+ <grade_categories>
+ <grade_category id="10">
+ <depth>1</depth>
+ </grade_category>
+ </grade_categories>
+</gradebook>
--- /dev/null
+<gradebook some_other_value="false" calculations_freeze="20160511">
+ <grade_categories>
+ <grade_category id="10">
+ <depth>1</depth>
+ </grade_category>
+ </grade_categories>
+</gradebook>
--- /dev/null
+<gradebook some_other_value="false" and_another_value="42">
+ <attributes>
+ <calculations_freeze>20160511</calculations_freeze>
+ </attributes>
+ <grade_categories>
+ <grade_category id="10">
+ <depth>1</depth>
+ </grade_category>
+ </grade_categories>
+</gradebook>
--- /dev/null
+<gradebook some_other_value="false" calculations_freeze="20160511" and_another_value="42">
+ <grade_categories>
+ <grade_category id="10">
+ <depth>1</depth>
+ </grade_category>
+ </grade_categories>
+</gradebook>
--- /dev/null
+<gradebookplugin some_other_value="false" calculations_freeze="20160511" and_another_value="42">
+ <grade_categories>
+ <grade_category id="10">
+ <depth>1</depth>
+ </grade_category>
+ </grade_categories>
+</gradebookplugin>
--- /dev/null
+<gradebookplugin some_other_value="false" calculations_freeze="20160511" and_another_value="42">
+ <grade_categories>
+ <grade_category id="10">
+ <depth>1</depth>
+ </grade_category>
+ </grade_categories>
+</gradebookplugin>
--- /dev/null
+<?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/>.
+
+/**
+ * Test for restore_stepslib.
+ *
+ * @package core_backup
+ * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
+require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
+require_once($CFG->libdir . '/completionlib.php');
+
+/**
+ * Test for restore_stepslib.
+ *
+ * @package core_backup
+ * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_backup_restore_gradebook_structure_step_testcase extends advanced_testcase {
+
+ /**
+ * Provide tests for rewrite_step_backup_file_for_legacy_freeze based upon fixtures.
+ *
+ * @return array
+ */
+ public function rewrite_step_backup_file_for_legacy_freeze_provider() {
+ $fixturesdir = realpath(__DIR__ . '/fixtures/rewrite_step_backup_file_for_legacy_freeze/');
+ $tests = [];
+ $iterator = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($fixturesdir),
+ \RecursiveIteratorIterator::LEAVES_ONLY);
+
+ foreach ($iterator as $sourcefile) {
+ $pattern = '/\.test$/';
+ if (!preg_match($pattern, $sourcefile)) {
+ continue;
+ }
+
+ $expectfile = preg_replace($pattern, '.expectation', $sourcefile);
+ $test = array($sourcefile, $expectfile);
+ $tests[basename($sourcefile)] = $test;
+ }
+
+ return $tests;
+ }
+
+ /**
+ * @dataProvider rewrite_step_backup_file_for_legacy_freeze_provider
+ * @param string $source The source file to test
+ * @param string $expected The expected result of the transformation
+ */
+ public function test_rewrite_step_backup_file_for_legacy_freeze($source, $expected) {
+ $restore = $this->getMockBuilder('\restore_gradebook_structure_step')
+ ->setMethods(null)
+ ->disableOriginalConstructor()
+ ->getMock()
+ ;
+
+ // Copy the file somewhere as the rewrite_step_backup_file_for_legacy_freeze will write the file.
+ $dir = make_request_directory(true);
+ $filepath = $dir . DIRECTORY_SEPARATOR . 'file.xml';
+ copy($source, $filepath);
+
+ $rc = new \ReflectionClass('\restore_gradebook_structure_step');
+ $rcm = $rc->getMethod('rewrite_step_backup_file_for_legacy_freeze');
+ $rcm->setAccessible(true);
+ $rcm->invoke($restore, $filepath);
+
+ // Check the result.
+ $this->assertFileEquals($expected, $filepath);
+ }
+}
--- /dev/null
+@block @block_blog_recent
+Feature: Feature: Users can use the recent blog entries block to view recent blog entries.
+ In order to enable the recent blog entries in a course
+ As a teacher
+ I can add recent blog entries block to a course
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email | idnumber |
+ | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+
+ Scenario: Add the recent blogs block to a course when blogs are disabled
+ Given I log in as "admin"
+ And the following config values are set as admin:
+ | enableblogs | 0 |
+ And I log out
+ And I log in as "teacher1"
+ And I follow "Course 1"
+ And I turn editing mode on
+ When I add the "Recent blog entries" block
+ Then I should see "Blogging is disabled!" in the "Recent blog entries" "block"
+
+ Scenario: Add the recent blogs block to a course when there are not any blog posts
+ Given I log in as "teacher1"
+ And I follow "Course 1"
+ And I turn editing mode on
+ When I add the "Recent blog entries" block
+ Then I should see "No recent entries" in the "Recent blog entries" "block"
--- /dev/null
+@block @block_blog_menu @mod_assign @block_blog_recent
+Feature: Students can use the recent blog entries block to view recent entries on an activity page
+ In order to enable the recent blog entries block an activity page
+ As a teacher
+ I can add the recent blog entries block to an activity page
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email | idnumber |
+ | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+ | student1 | Student | 1 | student1@example.com | S1 |
+ | student2 | Student | 2 | student2@example.com | S2 |
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ | student1 | C1 | student |
+ | student2 | C1 | student |
+ And I log in as "teacher1"
+ And I follow "Course 1"
+ And I turn editing mode on
+ And I add a "Assignment" to section "1" and I fill the form with:
+ | Assignment name | Test assignment 1 |
+ | Description | Offline text |
+ | assignsubmission_file_enabled | 0 |
+ And I follow "Test assignment 1"
+ And I add the "Blog menu" block
+ And I add the "Recent blog entries" block
+ And I log out
+
+ Scenario: Students use the recent blog entries block to view blogs
+ Given I log in as "student1"
+ And I follow "Course 1"
+ And I follow "Test assignment 1"
+ And I follow "Add an entry about this Assignment"
+ When I set the following fields to these values:
+ | Entry title | S1 First Blog |
+ | Blog entry body | This is my awesome blog! |
+ And I press "Save changes"
+ Then I should see "S1 First Blog"
+ And I should see "This is my awesome blog!"
+ And I follow "Test assignment 1"
+ And I should see "S1 First Blog"
+ And I follow "S1 First Blog"
+ And I should see "This is my awesome blog!"
+
+ Scenario: Students only see a few entries in the recent blog entries block
+ Given I log in as "student1"
+ And I follow "Course 1"
+ And I follow "Test assignment 1"
+ And I follow "Add an entry about this Assignment"
+ # Blog 1 of 5
+ And I set the following fields to these values:
+ | Entry title | S1 First Blog |
+ | Blog entry body | This is my awesome blog! |
+ And I press "Save changes"
+ And I wait "1" seconds
+ And I follow "Test assignment 1"
+ And I follow "Add an entry about this Assignment"
+ # Blog 2 of 5
+ And I set the following fields to these values:
+ | Entry title | S1 Second Blog |
+ | Blog entry body | This is my awesome blog! |
+ And I press "Save changes"
+ And I wait "1" seconds
+ And I should see "S1 Second Blog"
+ And I should see "This is my awesome blog!"
+ And I follow "Test assignment 1"
+ And I follow "Add an entry about this Assignment"
+ # Blog 3 of 5
+ And I set the following fields to these values:
+ | Entry title | S1 Third Blog |
+ | Blog entry body | This is my awesome blog! |
+ And I press "Save changes"
+ And I wait "1" seconds
+ And I should see "S1 Third Blog"
+ And I should see "This is my awesome blog!"
+ And I follow "Test assignment 1"
+ And I follow "Add an entry about this Assignment"
+ # Blog 4 of 5
+ And I set the following fields to these values:
+ | Entry title | S1 Fourth Blog |
+ | Blog entry body | This is my awesome blog! |
+ And I press "Save changes"
+ And I wait "1" seconds
+ And I should see "S1 Fourth Blog"
+ And I should see "This is my awesome blog!"
+ And I follow "Test assignment 1"
+ And I follow "Add an entry about this Assignment"
+ # Blog 5 of 5
+ And I set the following fields to these values:
+ | Entry title | S1 Fifth Blog |
+ | Blog entry body | This is my awesome blog! |
+ And I press "Save changes"
+ And I should see "S1 Fifth Blog"
+ And I should see "This is my awesome blog!"
+ When I follow "Test assignment 1"
+ And I should not see "S1 First Blog"
+ And I should see "S1 Second Blog"
+ And I should see "S1 Third Blog"
+ And I should see "S1 Fourth Blog"
+ And I should see "S1 Fifth Blog"
+ And I follow "S1 Fifth Blog"
+ And I should see "This is my awesome blog!"
+ Then I log out
+ And I log in as "teacher1"
+ And I follow "Course 1"
+ And I turn editing mode on
+ And I follow "Test assignment 1"
+ And I configure the "Recent blog entries" block
+ And I set the following fields to these values:
+ | id_config_numberofrecentblogentries | 2 |
+ And I press "Save changes"
+ And I should see "S1 Fourth Blog"
+ And I should see "S1 Fifth Blog"
--- /dev/null
+@block @block_blog_menu @block_blog_recent
+Feature: Students can use the recent blog entries block to view recent entries on a course page
+ In order to enable the recent blog entries block a course page
+ As a teacher
+ I can add the recent blog entries block to a course page
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email | idnumber |
+ | student1 | Student | 1 | student1@example.com | S1 |
+ | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ | student1 | C1 | student |
+ And I log in as "teacher1"
+ And I follow "Course 1"
+ And I turn editing mode on
+ And I add the "Blog menu" block
+ And I add the "Recent blog entries" block
+ And I log out
+
+ Scenario: Students use the recent blog entries block to view blogs
+ Given I log in as "student1"
+ And I follow "Course 1"
+ And I follow "Add an entry about this course"
+ When I set the following fields to these values:
+ | Entry title | S1 First Blog |
+ | Blog entry body | This is my awesome blog! |
+ And I press "Save changes"
+ Then I should see "S1 First Blog"
+ And I should see "This is my awesome blog!"
+ And I follow "C1"
+ And I should see "S1 First Blog"
+ And I follow "S1 First Blog"
+ And I should see "This is my awesome blog!"
+
+ Scenario: Students only see a few entries in the recent blog entries block
+ Given I log in as "student1"
+ And I follow "Course 1"
+ And I follow "Add an entry about this course"
+ # Blog 1 of 5
+ And I set the following fields to these values:
+ | Entry title | S1 First Blog |
+ | Blog entry body | This is my awesome blog! |
+ And I press "Save changes"
+ And I wait "1" seconds
+ And I follow "C1"
+ And I follow "Add an entry about this course"
+ # Blog 2 of 5
+ And I set the following fields to these values:
+ | Entry title | S1 Second Blog |
+ | Blog entry body | This is my awesome blog! |
+ And I press "Save changes"
+ And I wait "1" seconds
+ And I should see "S1 Second Blog"
+ And I should see "This is my awesome blog!"
+ And I follow "C1"
+ And I follow "Add an entry about this course"
+ # Blog 3 of 5
+ And I set the following fields to these values:
+ | Entry title | S1 Third Blog |
+ | Blog entry body | This is my awesome blog! |
+ And I press "Save changes"
+ And I wait "1" seconds
+ And I should see "S1 Third Blog"
+ And I should see "This is my awesome blog!"
+ And I follow "C1"
+ And I follow "Add an entry about this course"
+ # Blog 4 of 5
+ And I set the following fields to these values:
+ | Entry title | S1 Fourth Blog |
+ | Blog entry body | This is my awesome blog! |
+ And I press "Save changes"
+ And I wait "1" seconds
+ And I should see "S1 Fourth Blog"
+ And I should see "This is my awesome blog!"
+ And I follow "C1"
+ And I follow "Add an entry about this course"
+ # Blog 5 of 5
+ And I set the following fields to these values:
+ | Entry title | S1 Fifth Blog |
+ | Blog entry body | This is my awesome blog! |
+ And I press "Save changes"
+ And I should see "S1 Fifth Blog"
+ And I should see "This is my awesome blog!"
+ When I follow "C1"
+ And I should not see "S1 First Blog"
+ And I should see "S1 Second Blog"
+ And I should see "S1 Third Blog"
+ And I should see "S1 Fourth Blog"
+ And I should see "S1 Fifth Blog"
+ And I follow "S1 Fifth Blog"
+ And I should see "This is my awesome blog!"
+ Then I log out
+ And I log in as "teacher1"
+ And I follow "Course 1"
+ And I turn editing mode on
+ And I configure the "Recent blog entries" block
+ And I set the following fields to these values:
+ | id_config_numberofrecentblogentries | 2 |
+ And I press "Save changes"
+ And I should see "S1 Fourth Blog"
+ And I should see "S1 Fifth Blog"
--- /dev/null
+@block @block_blog_recent
+Feature: Feature: Students can use the recent blog entries block to view recent entries on the frontpage
+ In order to enable the recent blog entries block on the frontpage
+ As an admin
+ I can add the recent blog entries block to the frontpage
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email | idnumber |
+ | student1 | Student | 1 | student1@example.com | S1 |
+ And I log in as "admin"
+ And I am on site homepage
+ And I navigate to "Turn editing on" node in "Front page settings"
+ And I add the "Recent blog entries" block
+ And I log out
+
+ Scenario: Students use the recent blog entries block to view blogs
+ Given I log in as "student1"
+ And I am on site homepage
+ And I navigate to "Site blogs" node in "Site pages"
+ And I follow "Add a new entry"
+ When I set the following fields to these values:
+ | Entry title | S1 First Blog |
+ | Blog entry body | This is my awesome blog! |
+ And I press "Save changes"
+ Then I should see "S1 First Blog"
+ And I should see "This is my awesome blog!"
+ And I am on site homepage
+ And I should see "S1 First Blog"
+ And I follow "S1 First Blog"
+ And I should see "This is my awesome blog!"
+
+ Scenario: Students only see a few entries in the recent blog entries block
+ Given I log in as "student1"
+ And I am on site homepage
+ And I navigate to "Site blogs" node in "Site pages"
+ And I follow "Add a new entry"
+ # Blog 1 of 5
+ And I set the following fields to these values:
+ | Entry title | S1 First Blog |
+ | Blog entry body | This is my awesome blog! |
+ And I press "Save changes"
+ And I wait "1" seconds
+ And I follow "Add a new entry"
+ # Blog 2 of 5
+ And I set the following fields to these values:
+ | Entry title | S1 Second Blog |
+ | Blog entry body | This is my awesome blog! |
+ And I press "Save changes"
+ And I wait "1" seconds
+ And I should see "S1 Second Blog"
+ And I should see "This is my awesome blog!"
+ And I follow "Add a new entry"
+ # Blog 3 of 5
+ And I set the following fields to these values:
+ | Entry title | S1 Third Blog |
+ | Blog entry body | This is my awesome blog! |
+ And I press "Save changes"
+ And I wait "1" seconds
+ And I should see "S1 Third Blog"
+ And I should see "This is my awesome blog!"
+ And I follow "Add a new entry"
+ # Blog 4 of 5
+ And I set the following fields to these values:
+ | Entry title | S1 Fourth Blog |
+ | Blog entry body | This is my awesome blog! |
+ And I press "Save changes"
+ And I wait "1" seconds
+ And I should see "S1 Fourth Blog"
+ And I should see "This is my awesome blog!"
+ And I follow "Add a new entry"
+ # Blog 5 of 5
+ And I set the following fields to these values:
+ | Entry title | S1 Fifth Blog |
+ | Blog entry body | This is my awesome blog! |
+ And I press "Save changes"
+ And I should see "S1 Fifth Blog"
+ And I should see "This is my awesome blog!"
+ When I am on site homepage
+ And I should not see "S1 First Blog"
+ And I should see "S1 Second Blog"
+ And I should see "S1 Third Blog"
+ And I should see "S1 Fourth Blog"
+ And I should see "S1 Fifth Blog"
+ And I follow "S1 Fifth Blog"
+ And I should see "This is my awesome blog!"
+ Then I log out
+ And I log in as "admin"
+ And I am on site homepage
+ And I navigate to "Turn editing on" node in "Front page settings"
+ And I configure the "Recent blog entries" block
+ And I set the following fields to these values:
+ | id_config_numberofrecentblogentries | 2 |
+ And I press "Save changes"
+ And I should see "S1 Fourth Blog"
+ And I should see "S1 Fifth Blog"
--- /dev/null
+@block @block_comments
+Feature: Enable Block comments on the dashboard and view comments
+ In order to enable the comments block on a the dashboard
+ As a teacher
+ I can add the comments block to my dashboard
+
+ Background:
+ Given the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | Frist | teacher1@example.com |
+
+ Scenario: Add the comments block on the dashboard and add comments with Javascript disabled
+ When I log in as "teacher1"
+ And I press "Customise this page"
+ And I add the "Comments" block
+ And I follow "Show comments"
+ And I add "I'm a comment from the teacher" comment to comments block
+ Then I should see "I'm a comment from the teacher"
+
+ @javascript
+ Scenario: Add the comments block on the dashboard and add comments with Javascript enabled
+ When I log in as "teacher1"
+ And I press "Customise this page"
+ And I add the "Comments" block
+ And I add "I'm a comment from the teacher" comment to comments block
+ Then I should see "I'm a comment from the teacher"
--- /dev/null
+@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"
+
if ($configdata = $DB->get_field('block_instances', 'configdata', array('id' => $blockid))) {
$config = unserialize(base64_decode($configdata));
if (!empty($config->glossary)) {
- // Get glossary mapping and replace it in config
if ($glossarymap = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'glossary', $config->glossary)) {
- $mappedglossary = $DB->get_record('glossary', array('id' => $glossarymap->newitemid),
- 'id,course,globalglossary', MUST_EXIST);
- $config->glossary = $mappedglossary->id;
- $config->courseid = $mappedglossary->course;
- $config->globalglossary = $mappedglossary->globalglossary;
- $configdata = base64_encode(serialize($config));
- $DB->set_field('block_instances', 'configdata', $configdata, array('id' => $blockid));
+ // Get glossary mapping and replace it in config
+ $config->glossary = $glossarymap->newitemid;
+ } else if ($this->is_samesite()) {
+ // We are restoring on the same site, check if glossary can be used in the block in this course.
+ $glossaryid = $DB->get_field_sql("SELECT id FROM {glossary} " .
+ "WHERE id = ? AND (course = ? OR globalglossary = 1)",
+ [$config->glossary, $this->get_courseid()]);
+ if (!$glossaryid) {
+ unset($config->glossary);
+ }
} else {
// The block refers to a glossary not present in the backup file.
- $DB->set_field('block_instances', 'configdata', '', array('id' => $blockid));
+ unset($config->glossary);
}
+ // Unset config variables that are no longer used.
+ unset($config->globalglossary);
+ unset($config->courseid);
+ // Save updated config.
+ $configdata = base64_encode(serialize($config));
+ $DB->set_field('block_instances', 'configdata', $configdata, array('id' => $blockid));
}
}
}
class block_glossary_random extends block_base {
+ /**
+ * @var cm_info|stdClass has properties 'id' (course module id) and 'uservisible'
+ * (whether the glossary is visible to the current user)
+ */
+ protected $glossarycm = null;
+
function init() {
$this->title = get_string('pluginname','block_glossary_random');
}
//check if it's time to put a new entry in cache
if (time() > $this->config->nexttime) {
+ if (!($cm = $this->get_glossary_cm()) || !$cm->uservisible) {
+ // Skip generating of the cache if we can't display anything to the current user.
+ return false;
+ }
+
// place glossary concept and definition in $pref->cache
if (!$numberofentries = $DB->count_records('glossary_entries',
array('glossaryid'=>$this->config->glossary, 'approved'=>1))) {
$this->instance_config_commit();
}
- // Get glossary instance, if not found then return without error, as this will be handled in get_content.
- if (!$glossary = $DB->get_record('glossary', array('id' => $this->config->glossary))) {
- return false;
- }
-
- $this->config->globalglossary = $glossary->globalglossary;
-
- // Save course id in config, so we can get correct course module.
- $this->config->courseid = $glossary->course;
-
- // Get module and context, to be able to rewrite urls
- if (! $cm = get_coursemodule_from_instance('glossary', $glossary->id, $this->config->courseid)) {
- return false;
- }
$glossaryctx = context_module::instance($cm->id);
$limitfrom = 0;
}
}
- function instance_allow_multiple() {
- // Are you going to allow multiple instances of each block?
- // If yes, then it is assumed that the block WILL USE per-instance configuration
- return true;
+ /**
+ * Replace the instance's configuration data with those currently in $this->config;
+ */
+ function instance_config_commit($nolongerused = false) {
+ // Unset config variables that are no longer used.
+ unset($this->config->globalglossary);
+ unset($this->config->courseid);
+ parent::instance_config_commit($nolongerused);
}
- function get_content() {
- global $USER, $CFG, $DB;
-
+ /**
+ * Checks if glossary is available - it should be either located in the same course or be global
+ *
+ * @return null|cm_info|stdClass object with properties 'id' (course module id) and 'uservisible'
+ */
+ protected function get_glossary_cm() {
+ global $DB;
if (empty($this->config->glossary)) {
- $this->content = new stdClass();
- if ($this->user_can_edit()) {
- $this->content->text = get_string('notyetconfigured','block_glossary_random');
- } else {
- $this->content->text = '';
- }
- $this->content->footer = '';
- return $this->content;
+ // No glossary is configured.
+ return null;
}
- require_once($CFG->dirroot.'/course/lib.php');
+ if (!empty($this->glossarycm)) {
+ return $this->glossarycm;
+ }
- // If $this->config->globalglossary is not set then get glossary info from db.
- if (!isset($this->config->globalglossary)) {
- if (!$glossary = $DB->get_record('glossary', array('id' => $this->config->glossary))) {
- return '';
- } else {
- $this->config->courseid = $glossary->course;
- $this->config->globalglossary = $glossary->globalglossary;
- $this->instance_config_commit();
+ if (!empty($this->page->course->id)) {
+ // First check if glossary belongs to the current course (we don't need to make any DB queries to find it).
+ $modinfo = get_fast_modinfo($this->page->course);
+ if (isset($modinfo->instances['glossary'][$this->config->glossary])) {
+ $this->glossarycm = $modinfo->instances['glossary'][$this->config->glossary];
+ if ($this->glossarycm->uservisible) {
+ // The glossary is in the same course and is already visible to the current user,
+ // no need to check if it is global, save on DB query.
+ return $this->glossarycm;
+ }
}
}
- $modinfo = get_fast_modinfo($this->config->courseid);
- // If deleted glossary or non-global glossary on different course page, then reset.
- if (!isset($modinfo->instances['glossary'][$this->config->glossary])
- || ((empty($this->config->globalglossary) && ($this->config->courseid != $this->page->course->id)))) {
+ // Find course module id for the given glossary, only if it is global.
+ $cm = $DB->get_record_sql("SELECT cm.id, cm.visible AS uservisible
+ FROM {course_modules} cm
+ JOIN {modules} md ON md.id = cm.module
+ JOIN {glossary} g ON g.id = cm.instance
+ WHERE g.id = :instance AND md.name = :modulename AND g.globalglossary = 1",
+ ['instance' => $this->config->glossary, 'modulename' => 'glossary']);
+
+ if ($cm) {
+ // This is a global glossary, create an object with properties 'id' and 'uservisible'. We don't need any
+ // other information so why bother retrieving it. Full access check is skipped for global glossaries for
+ // performance reasons.
+ $this->glossarycm = $cm;
+ } else if (empty($this->glossarycm)) {
+ // Glossary does not exist. Remove it in the config so we don't repeat this check again later.
$this->config->glossary = 0;
- $this->config->cache = '';
$this->instance_config_commit();
+ }
- $this->content = new stdClass();
- if ($this->user_can_edit()) {
- $this->content->text = get_string('notyetconfigured','block_glossary_random');
- } else {
- $this->content->text = '';
- }
- $this->content->footer = '';
+ return $this->glossarycm;
+ }
+
+ function instance_allow_multiple() {
+ // Are you going to allow multiple instances of each block?
+ // If yes, then it is assumed that the block WILL USE per-instance configuration
+ return true;
+ }
+
+ function get_content() {
+ if ($this->content !== null) {
return $this->content;
}
+ $this->content = (object)['text' => '', 'footer' => ''];
- $cm = $modinfo->instances['glossary'][$this->config->glossary];
- if (!has_capability('mod/glossary:view', context_module::instance($cm->id))) {
- return '';
+ if (!$cm = $this->get_glossary_cm()) {
+ if ($this->user_can_edit()) {
+ $this->content->text = get_string('notyetconfigured', 'block_glossary_random');
+ }
+ return $this->content;
}
if (empty($this->config->cache)) {
$this->config->cache = '';
}
- if ($this->content !== NULL) {
- return $this->content;
- }
-
- $this->content = new stdClass();
-
- // Show glossary if visible and place links in footer.
- if ($cm->visible) {
+ if ($cm->uservisible) {
+ // Show glossary if visible and place links in footer.
$this->content->text = $this->config->cache;
if (has_capability('mod/glossary:write', context_module::instance($cm->id))) {
- $this->content->footer = '<a href="'.$CFG->wwwroot.'/mod/glossary/edit.php?cmid='.$cm->id
- .'" title="'.$this->config->addentry.'">'.$this->config->addentry.'</a><br />';
- } else {
- $this->content->footer = '';
+ $this->content->footer = html_writer::link(new moodle_url('/mod/glossary/edit.php', ['cmid' => $cm->id]),
+ format_string($this->config->addentry)) . '<br/>';
}
- $this->content->footer .= '<a href="'.$CFG->wwwroot.'/mod/glossary/view.php?id='.$cm->id
- .'" title="'.$this->config->viewglossary.'">'.$this->config->viewglossary.'</a>';
-
- // Otherwise just place some text, no link.
+ $this->content->footer .= html_writer::link(new moodle_url('/mod/glossary/view.php', ['id' => $cm->id]),
+ format_string($this->config->viewglossary));
} else {
- $this->content->footer = $this->config->invisible;
+ // Otherwise just place some text, no link.
+ $this->content->footer = format_string($this->config->invisible);
}
return $this->content;
--- /dev/null
+@block @block_glossary_random
+Feature: Random glossary entry block linking to global glossary
+ In order to show the entries from glossary
+ As a teacher
+ I can add the random glossary entry to a course page
+
+ Background:
+ Given the following "courses" exist:
+ | fullname | shortname |
+ | Course 1 | C1 |
+ | Course 2 | C2 |
+ And the following "activities" exist:
+ | activity | name | intro | course | idnumber | globalglossary | defaultapproval |
+ | glossary | Tips and Tricks | Frontpage glossary description | C2 | glossary0 | 1 | 1 |
+ And the following "users" exist:
+ | username | firstname | lastname | email |
+ | student1 | Sam1 | Student1 | student1@example.com |
+ | teacher1 | Terry1 | Teacher1 | teacher1@example.com |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | student1 | C1 | student |
+ | teacher1 | C1 | editingteacher |
+
+ Scenario: View random (last) entry in the global glossary
+ When I log in as "admin"
+ And I am on site homepage
+ And I follow "Course 2"
+ And I follow "Tips and Tricks"
+ And I press "Add a new entry"
+ And I set the following fields to these values:
+ | Concept | Never come late |
+ | Definition | Come in time for your classes |
+ And I press "Save changes"
+ And I log out
+ # As a teacher add a block to the course page linking to the global glossary.
+ And I log in as "teacher1"
+ And I follow "Course 1"
+ And I turn editing mode on
+ And I add the "Random glossary entry" block
+ And I configure the "block_glossary_random" block
+ And I set the following fields to these values:
+ | Title | Tip of the day |
+ | Take entries from this glossary | Tips and Tricks |
+ | How a new entry is chosen | Last modified entry |
+ And I press "Save changes"
+ Then I should see "Never come late" in the "Tip of the day" "block"
+ And I should not see "Add a new entry" in the "Tip of the day" "block"
+ And I should see "View all entries" in the "Tip of the day" "block"
+ And I log out
+ # Student who can't see the module is still able to view entries in this block (because the glossary was marked as global)
+ And I log in as "student1"
+ And I follow "Course 1"
+ And I should see "Never come late" in the "Tip of the day" "block"
+ And I should not see "Add a new entry" in the "Tip of the day" "block"
+ And I should see "View all entries" in the "Tip of the day" "block"
+ And I log out
+
+ Scenario: Removing the global glossary that is used in random glossary block
+ And I log in as "teacher1"
+ And I follow "Course 1"
+ And I turn editing mode on
+ And I add the "Random glossary entry" block
+ And I configure the "block_glossary_random" block
+ And I set the following fields to these values:
+ | Title | Tip of the day |
+ | Take entries from this glossary | Tips and Tricks |
+ | How a new entry is chosen | Last modified entry |
+ And I press "Save changes"
+ And I log out
+ And I log in as "admin"
+ And I am on site homepage
+ And I follow "Course 2"
+ And I follow "Tips and Tricks"
+ And I follow "Edit settings"
+ And I set the field "globalglossary" to "0"
+ And I press "Save and return to course"
+ And I am on site homepage
+ And I follow "Course 1"
+ Then I should see "Please configure this block using the edit icon." in the "Tip of the day" "block"
+ And I log out
+ And I log in as "student1"
+ And I follow "Course 1"
+ And "Tip of the day" "block" should not exist
+ And I log out
--- /dev/null
+@block @block_messages
+Feature: The messages block allows users to list new messages an a course
+ In order to enable the messages block in a course
+ As a teacher
+ I can add the messages block to a course and view my messages
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email | idnumber |
+ | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+ | student1 | Student | 1 | student1@example.com | S1 |
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ | student1 | C1 | student |
+
+ Scenario: View the block by a user with messaging disabled.
+ Given the following config values are set as admin:
+ | messaging | 0 |
+ And I log in as "teacher1"
+ And I follow "Course 1"
+ When I turn editing mode on
+ And I add the "Messages" block
+ Then I should see "Messaging is disabled on this site" in the "Messages" "block"
+
+ Scenario: View the block by a user who does not have any messages.
+ Given I log in as "teacher1"
+ And I follow "Course 1"
+ When I turn editing mode on
+ And I add the "Messages" block
+ Then I should see "No messages waiting" in the "Messages" "block"
+
+ Scenario: View the block by a user who has messages.
+ Given I log in as "student1"
+ And I follow "Messages" in the user menu
+ And I send "This is message 1" message to "Teacher 1" user
+ And I send "This is message 2" message to "Teacher 1" user
+ And I log out
+ And I log in as "teacher1"
+ And I follow "Course 1"
+ When I turn editing mode on
+ And I add the "Messages" block
+ Then I should see "Student 1" in the "Messages" "block"
+
+ Scenario: Use the block to send a message to a user.
+ Given I log in as "teacher1"
+ And I follow "Course 1"
+ And I turn editing mode on
+ And I add the "Messages" block
+ And I follow "Messages"
+ And I send "This is message 1" message to "Student 1" user
+ And I log out
+ When I log in as "student1"
+ And I follow "Course 1"
+ Then I should see "Teacher 1" in the "Messages" "block"
--- /dev/null
+@block @block_messages
+Feature: The messages block allows users to list new messages on the dashboard
+ In order to enable the messages block on the dashboard
+ As a user
+ I can add the messages block to a my dashboard and view my messages
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email | idnumber |
+ | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+ | student1 | Student | 1 | student1@example.com | S1 |
+
+ Scenario: View the block by a user with messaging disabled.
+ Given the following config values are set as admin:
+ | messaging | 0 |
+ And I log in as "teacher1"
+ And I press "Customise this page"
+ When I add the "Messages" block
+ Then I should see "Messaging is disabled on this site" in the "Messages" "block"
+
+ Scenario: View the block by a user who does not have any messages.
+ Given I log in as "teacher1"
+ And I press "Customise this page"
+ When I add the "Messages" block
+ Then I should see "No messages waiting" in the "Messages" "block"
+
+ Scenario: View the block by a user who has messages.
+ Given I log in as "student1"
+ And I follow "Messages" in the user menu
+ And I send "This is message 1" message to "Teacher 1" user
+ And I send "This is message 2" message to "Teacher 1" user
+ And I log out
+ When I log in as "teacher1"
+ And I press "Customise this page"
+ And I add the "Messages" block
+ Then I should see "Student 1" in the "Messages" "block"
+
+ Scenario: Use the block to send a message to a user.
+ Given I log in as "teacher1"
+ And I press "Customise this page"
+ And I add the "Messages" block
+ And I follow "Messages"
+ And I send "This is message 1" message to "Student 1" user
+ And I log out
+ When I log in as "student1"
+ And I press "Customise this page"
+ And I add the "Messages" block
+ Then I should see "Teacher 1" in the "Messages" "block"
--- /dev/null
+@block @block_messages
+Feature: The messages block allows users to list new messages on the frontpage
+ In order to enable the messages block on the frontpage
+ As an admin
+ I can add the messages block to a the frontpage and view my messages
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email | idnumber |
+ | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+ | student1 | Student | 1 | student1@example.com | S1 |
+ And I log in as "admin"
+ And I am on site homepage
+ And I navigate to "Turn editing on" node in "Front page settings"
+ And I add the "Messages" block
+ And I log out
+
+ Scenario: View the block by a user with messaging disabled.
+ Given the following config values are set as admin:
+ | messaging | 0 |
+ And I log in as "admin"
+ And I am on site homepage
+ When I navigate to "Turn editing on" node in "Front page settings"
+ And I should see "Messaging is disabled on this site" in the "Messages" "block"
+ Then I navigate to "Turn editing off" node in "Front page settings"
+ And I should not see "Messaging is disabled on this site"
+
+ Scenario: View the block by a user who does not have any messages.
+ Given I log in as "teacher1"
+ When I am on site homepage
+ Then I should see "No messages waiting" in the "Messages" "block"
+
+ Scenario: Try to view the block as a guest user.
+ Given I log in as "guest"
+ When I am on site homepage
+ Then I should not see "Messages"
+
+ Scenario: View the block by a user who has messages.
+ Given I log in as "student1"
+ And I follow "Messages" in the user menu
+ And I send "This is message 1" message to "Teacher 1" user
+ And I send "This is message 2" message to "Teacher 1" user
+ And I log out
+ When I log in as "teacher1"
+ And I am on site homepage
+ Then I should see "Student 1" in the "Messages" "block"
+
+ Scenario: Use the block to send a message to a user.
+ Given I log in as "teacher1"
+ And I am on site homepage
+ And I follow "Messages"
+ And I send "This is message 1" message to "Student 1" user
+ And I log out
+ When I log in as "student1"
+ And I am on site homepage
+ Then I should see "Teacher 1" in the "Messages" "block"
*/
define(['jquery', 'core/tree'], function($, Tree) {
return {
- init: function() {
- new Tree(".block_navigation .block_tree");
+ init: function(instanceid) {
+ var navTree = new Tree(".block_navigation .block_tree");
+ navTree.finishExpandingGroup = function(item) {
+ Tree.prototype.finishExpandingGroup.call(this, item);
+ Y.use('moodle-core-event', function() {
+ Y.Global.fire(M.core.globalEvents.BLOCK_CONTENT_UPDATED, {
+ instanceid: instanceid
+ });
+ });
+ };
+ navTree.collapseGroup = function(item) {
+ Tree.prototype.collapseGroup.call(this, item);
+ Y.use('moodle-core-event', function() {
+ Y.Global.fire(M.core.globalEvents.BLOCK_CONTENT_UPDATED, {
+ instanceid: instanceid
+ });
+ });
+ };
}
};
});
*/
function get_required_javascript() {
parent::get_required_javascript();
+ $arguments = array(
+ 'instanceid' => $this->instance->id
+ );
$this->page->requires->string_for_js('viewallcourses', 'moodle');
- $this->page->requires->js_call_amd('block_navigation/navblock', 'init', array());
+ $this->page->requires->js_call_amd('block_navigation/navblock', 'init', $arguments);
}
/**
.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;}
$groupmode = groups_get_activity_groupmode($cm);
$currentgroup = groups_get_activity_group($cm, true);
-
if (forum_user_can_post_discussion($forum, $currentgroup, $groupmode, $cm, $context)) {
$text .= '<div class="newlink"><a href="'.$CFG->wwwroot.'/mod/forum/post.php?forum='.$forum->id.'">'.
get_string('addanewtopic', 'forum').'</a>...</div>';
// This sort will ignore pinned posts as we want the most recent.
$sort = forum_get_default_sort_order(true, 'p.modified', 'd', false);
if (! $discussions = forum_get_discussions($cm, $sort, false,
- $currentgroup, $this->page->course->newsitems) ) {
+ -1, $this->page->course->newsitems,
+ false, -1, 0, FORUM_POSTS_ALL_USER_GROUPS) ) {
$text .= '('.get_string('nonews', 'forum').')';
$this->content->text = $text;
return $this->content;
--- /dev/null
+@block @block_online_users
+Feature: The online users block allow you to see who is currently online
+ In order to enable the online users block on an course page
+ As a teacher
+ I can add the online users block to a course page
+
+ Background:
+ Given the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ | student1 | Student | 1 | student1@example.com |
+ | student2 | Student | 2 | student2@example.com |
+
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ | student1 | C1 | student |
+
+ Scenario: Add the online users on course page and see myself
+ Given I log in as "teacher1"
+ And I follow "Course 1"
+ And I turn editing mode on
+ When I add the "Online users" block
+ Then I should see "Teacher 1" in the "Online users" "block"
+
+ Scenario: Add the online users on course page and see other logged in users
+ Given I log in as "teacher1"
+ And I follow "Course 1"
+ And I turn editing mode on
+ And I add the "Online users" block
+ And I log out
+ And I log in as "student2"
+ And I log out
+ When I log in as "student1"
+ And I follow "Course 1"
+ Then I should see "Teacher 1" in the "Online users" "block"
+ And I should see "Student 1" in the "Online users" "block"
+ And I should not see "Student 2" in the "Online users" "block"
--- /dev/null
+@block @block_online_users
+Feature: The online users block allow you to see who is currently online
+ In order to use the online users block on the dashboard
+ As a user
+ I can view the online users block on my dashboard
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ | student1 | Student | 1 | student1@example.com |
+ | student2 | Student | 2 | student2@example.com |
+
+ Scenario: View the online users block on the dashboard and see myself
+ Given I log in as "teacher1"
+ Then I should see "Teacher 1" in the "Online users" "block"
+
+ Scenario: View the online users block on the dashboard and see other logged in users
+ Given I log in as "student2"
+ And I log out
+ And I log in as "student1"
+ And I log out
+ When I log in as "teacher1"
+ Then I should see "Teacher 1" in the "Online users" "block"
+ And I should see "Student 1" in the "Online users" "block"
+ And I should see "Student 2" in the "Online users" "block"
--- /dev/null
+@block @block_online_users
+Feature: The online users block allow you to see who is currently online
+ In order to enable the online users block on the front page page
+ As an admin
+ I can add the online users block to the front page page
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | student1 | Student | 1 | student1@example.com |
+ | student2 | Student | 2 | student2@example.com |
+
+ Scenario: View the online users block on the front page and see myself
+ Given I log in as "admin"
+ And I am on site homepage
+ And I navigate to "Turn editing on" node in "Front page settings"
+ When I add the "Online users" block
+ Then I should see "Admin User" in the "Online users" "block"
+
+ Scenario: View the online users block on the front page as a logged in user
+ Given I log in as "admin"
+ And I am on site homepage
+ And I navigate to "Turn editing on" node in "Front page settings"
+ And I add the "Online users" block
+ And I log out
+ And I log in as "student2"
+ And I log out
+ When I log in as "student1"
+ And I am on site homepage
+ Then I should see "Admin User" in the "Online users" "block"
+ And I should see "Student 1" in the "Online users" "block"
+ And I should see "Student 2" in the "Online users" "block"
+
+ Scenario: View the online users block on the front page as a guest
+ Given I log in as "admin"
+ And I am on site homepage
+ And I navigate to "Turn editing on" node in "Front page settings"
+ And I add the "Online users" block
+ And I log out
+ And I log in as "student2"
+ And I log out
+ And I log in as "student1"
+ And I log out
+ When I log in as "guest"
+ And I am on site homepage
+ Then I should see "Admin User" in the "Online users" "block"
+ And I should see "Student 1" in the "Online users" "block"
+ And I should see "Student 2" in the "Online users" "block"
--- /dev/null
+@block @block_private_files @file_upload @javascript
+Feature: The private files block allows users to store files privately in moodle
+ In order to store a private file in moodle
+ As a teacher
+ I can upload the file to my private files area using the private files block in an activity
+
+ Scenario: Upload a file to the private files block in an activity
+ Given the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ And the following "activities" exist:
+ | activity | course | idnumber | name | intro |
+ | page | C1 | page1 | Test page name | Test page description |
+ And I log in as "teacher1"
+ And I follow "Course 1"
+ And I turn editing mode on
+ And I follow "Test page name"
+ And I add the "Private files" block
+ And I should see "No files available" in the "Private files" "block"
+ When I follow "Manage private files..."
+ And I upload "blocks/private_files/tests/fixtures/testfile.txt" file to "Files" filemanager
+ And I press "Save changes"
+ Then I should see "testfile.txt" in the "Private files" "block"
--- /dev/null
+@block @block_private_files @file_upload @javascript
+Feature: The private files block allows users to store files privately in moodle
+ In order to store a private file in moodle
+ As a teacher
+ I can upload the file to my private files area using the private files block in a course
+
+ Scenario: Upload a file to the private files block from a course
+ Given the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ And I log in as "teacher1"
+ And I follow "Course 1"
+ And I turn editing mode on
+ And I add the "Private files" block
+ And I should see "No files available" in the "Private files" "block"
+ When I follow "Manage private files..."
+ And I upload "blocks/private_files/tests/fixtures/testfile.txt" file to "Files" filemanager
+ And I press "Save changes"
+ Then I should see "testfile.txt" in the "Private files" "block"
--- /dev/null
+@block @block_private_files @file_upload @javascript
+Feature: The private files block allows users to store files privately in moodle
+ In order to store a private file in moodle
+ As a user
+ I can upload the file to my private files area using the private files block on the dashboard
+
+ Scenario: Upload a file to the private files block from the dashboard
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ And I log in as "teacher1"
+ And "Private files" "block" should exist
+ And I should see "No files available" in the "Private files" "block"
+ When I follow "Manage private files..."
+ And I upload "blocks/private_files/tests/fixtures/testfile.txt" file to "Files" filemanager
+ And I press "Save changes"
+ Then I should see "testfile.txt" in the "Private files" "block"
--- /dev/null
+@block @block_private_files @file_upload
+Feature: The private files block allows users to store files privately in moodle
+ In order to store a private file in moodle
+ As a teacher
+ I can upload the file to my private files area using the private files block from the front page
+
+ Background:
+ Given the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ And I log in as "admin"
+ And I am on site homepage
+ And I navigate to "Turn editing on" node in "Front page settings"
+ And I add the "Private files" block
+ And I log out
+
+ Scenario: Try to view the private files block as a guest
+ Given I log in as "guest"
+ When I am on site homepage
+ Then "Private files" "block" should not exist
+
+ @javascript
+ Scenario: Upload a file to the private files block from the frontpage
+ Given I log in as "teacher1"
+ And I am on site homepage
+ And "Private files" "block" should exist
+ And I should see "No files available" in the "Private files" "block"
+ When I follow "Manage private files..."
+ And I upload "blocks/private_files/tests/fixtures/testfile.txt" file to "Files" filemanager
+ And I press "Save changes"
+ Then I should see "testfile.txt" in the "Private files" "block"
--- /dev/null
+This is a test file
/**
* Load the settings block tree javscript
*
- * @module block_navigation/navblock
+ * @module block_settings/settingsblock
* @package core
* @copyright 2015 John Okely <john@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/tree'], function($, Tree) {
return {
- init: function(siteAdminNodeId) {
+ init: function(instanceid, siteAdminNodeId) {
var adminTree = new Tree(".block_settings .block_tree");
if (siteAdminNodeId) {
var siteAdminNode = adminTree.treeRoot.find('#' + siteAdminNodeId);
var siteAdminLink = siteAdminNode.children('a').first();
siteAdminLink.replaceWith('<span tabindex="0">' + siteAdminLink.html() + '</span>');
}
+ adminTree.finishExpandingGroup = function(item) {
+ Tree.prototype.finishExpandingGroup.call(this, item);
+ Y.use('moodle-core-event', function() {
+ Y.Global.fire(M.core.globalEvents.BLOCK_CONTENT_UPDATED, {
+ instanceid: instanceid
+ });
+ });
+ };
+ adminTree.collapseGroup = function(item) {
+ Tree.prototype.collapseGroup.call(this, item);
+ Y.Global.fire(M.core.globalEvents.BLOCK_CONTENT_UPDATED, {
+ instanceid: instanceid
+ });
+ Y.use('moodle-core-event', function() {
+ Y.Global.fire(M.core.globalEvents.BLOCK_CONTENT_UPDATED, {
+ instanceid: instanceid
+ });
+ });
+ };
}
};
});
function get_required_javascript() {
global $PAGE;
- $adminnodeid = null;
$adminnode = $PAGE->settingsnav->find('siteadministration', navigation_node::TYPE_SITE_ADMIN);
- if (!empty($adminnode)) {
- $adminnodeid = $adminnode->id;
- }
parent::get_required_javascript();
$arguments = array(
- 'id' => $this->instance->id,
- 'instance' => $this->instance->id,
- 'candock' => $this->instance_can_be_docked()
+ 'instanceid' => $this->instance->id,
+ 'adminnodeid' => $adminnode ? $adminnode->id : null
);
- $this->page->requires->js_call_amd('block_settings/settingsblock', 'init', array($adminnodeid));
+ $this->page->requires->js_call_amd('block_settings/settingsblock', 'init', $arguments);
}
/**
.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;}
require_login($course, true, $cm);
require_sesskey();
+if (!$course) {
+ // Require_login() does not set context if called without a $course, do it manually.
+ $PAGE->set_context($context);
+}
+
$action = optional_param('action', '', PARAM_ALPHA);
$area = optional_param('area', '', PARAM_AREA);
$content = optional_param('content', '', PARAM_RAW);
$cmt = new stdClass;
$cmt->contextid = $contextid;
-$cmt->courseid = $course->id;
+if ($course) {
+ $cmt->courseid = $course->id;
+}
$cmt->cm = $cm;
$cmt->area = $area;
$cmt->itemid = $itemid;
'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),
)
}
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;
}
break;
case 'move':
case 'update':
- case 'duplicate':
case 'assignroles':
break;
default:
}
$roleid = optional_param('role', null, PARAM_INT);
- $duration = optional_param('duration', 0, PARAM_INT);
+ $duration = optional_param('duration', 0, PARAM_FLOAT);
$startdate = optional_param('startdate', 0, PARAM_INT);
$recovergrades = optional_param('recovergrades', 0, PARAM_INT);
if ($duration <= 0) {
$timeend = 0;
} else {
- $timeend = $timestart + ($duration*24*60*60);
+ $timeend = $timestart + intval($duration*24*60*60);
}
$instances = $manager->get_enrolment_instances();
$today = make_timestamp(date('Y', $now), date('m', $now), date('d', $now), 0, 0, 0);
$startdateoptions[3] = get_string('today') . ' (' . userdate($today, $dateformat) . ')';
$startdateoptions[4] = get_string('now', 'enrol_manual') . ' (' . userdate($now, get_string('strftimedatetimeshort')) . ')';
- $defaultduration = $instance->enrolperiod > 0 ? $instance->enrolperiod / 86400 : '';
+ $defaultduration = $instance->enrolperiod > 0 ? $instance->enrolperiod / DAYSECS : '';
$modules = array('moodle-enrol_manual-quickenrolment', 'moodle-enrol_manual-quickenrolment-skin');
$arguments = array(
} else {
$defaultperiod = $instance->enrolperiod;
}
+if ($instance->enrolperiod > 0 && !isset($periodmenu[$instance->enrolperiod])) {
+ $periodmenu[$instance->enrolperiod] = format_time($instance->enrolperiod);
+}
if (empty($extendbase)) {
if (!$extendbase = get_config('enrol_manual', 'enrolstart')) {
// Default to now if there is no system setting.
populateDuration : function() {
var select = this.get(UEP.BASE).one('.'+CSS.ENROLMENTOPTION+'.'+CSS.DURATION+' select');
var defaultvalue = this.get(UEP.DEFAULTDURATION);
+ var prefix = Math.round(defaultvalue) != defaultvalue ? '≈' : '';
var index = 0, count = 0;
var durationdays = M.util.get_string('durationdays', 'enrol', '{a}');
for (var i = 1; i <= 365; i++) {
}
select.append(option);
}
+ if (!index && defaultvalue > 0) {
+ select.append(create('<option value="'+defaultvalue+'">'+durationdays.replace('{a}',
+ prefix + (Math.round(defaultvalue * 100) / 100))+'</option>'));
+ index = ++count;
+ }
select.set('selectedIndex', index);
},
getAssignableRoles : function(){
$icon->pix = 'i/outcomes';
$icon->title = s(get_string('outcome', 'grades'));
} else {
- $icon->pix = 'icon';
- $icon->component = $element['object']->itemmodule;
+ $modinfo = get_fast_modinfo($element['object']->courseid);
+ $module = $element['object']->itemmodule;
+ $instanceid = $element['object']->iteminstance;
+ if (isset($modinfo->instances[$module][$instanceid])) {
+ $icon->url = $modinfo->instances[$module][$instanceid]->get_icon_url();
+ } else {
+ $icon->pix = 'icon';
+ $icon->component = $element['object']->itemmodule;
+ }
$icon->title = s(get_string('modulename', $element['object']->itemmodule));
}
} else if ($element['object']->itemtype == 'manual') {
if ($spacerifnone) {
$outputstr = $OUTPUT->spacer() . ' ';
}
+ } else if (isset($icon->url)) {
+ $outputstr = html_writer::img($icon->url, $icon->title, $icon->attributes);
} else {
$outputstr = $OUTPUT->pix_icon($icon->pix, $icon->title, $icon->component, $icon->attributes);
}
$string['cliyesnoprompt'] = 'Escriu y (significa Sí) o n (significa No)';
$string['environmentrequireinstall'] = 'cal instal·lar-lo i habilitar-lo';
$string['environmentrequireversion'] = 'esteu executant la versió {$a->current} i es requereix la {$a->needed}';
+$string['upgradekeyset'] = 'Clau d\'actualització (deixeu-ho en blanc per no establir-ne cap)';
$string['pathshead'] = 'Confirmeu els camins';
$string['pathsrodataroot'] = 'No es pot escriure en el directori dataroot.';
$string['pathsroparentdataroot'] = 'No es pot escriure en el directori pare ({$a->parent}). L\'instal·lador no pot crear el directori ({$a->dataroot}).';
-$string['pathssubadmindir'] = 'Alguns serveis d\'allotjament web (pocs) utilitzen un URL especial /admin p. ex. per a accedir a un tauler de control o quelcom semblant. Malauradament això entra en conflicte amb la ubicació estàndard de les pàgines d\'administració de Moodle. Podeu arreglar aquest problema canviant el nom del directori d\'administració de Moodle en la vostra instal·lació i posant el nou nom aquí. Per exemple <em>moodleadmin</em>. Això modificarà els enllaços d\'administració de Moodle.';
+$string['pathssubadmindir'] = 'Alguns serveis d\'allotjament web (pocs) utilitzen /admin com a URL especial perquè accediu a un tauler de control o quelcom semblant. Malauradament, això entra en conflicte amb la ubicació estàndard de les pàgines d\'administració de Moodle. Podeu arreglar aquest problema canviant el nom del directori d\'administració de Moodle en la vostra instal·lació i posant el nou nom aquí. Per exemple: <em>moodleadmin</em>. Això arreglarà els enllaços d\'administració de Moodle.';
$string['pathssubdataroot'] = 'Necessiteu un espai on Moodle pugui desar els fitxers penjats. Aquest directori hauria de tenir permisos de lectura I ESCRIPTURA per a l\'usuari del servidor web (normalment \'nobody\' o \'apache\'), però no cal que sigui accessible directament via web. L\'instal·lador provarà de crear-lo si no existeix.';
$string['pathssubdirroot'] = 'Camí complet del directori d\'instal·lació de Moodle.';
-$string['pathssubwwwroot'] = 'L\'adreça web completa on s\'accedirà a Moodle.
-No és possible accedir a Moodle en diferents adreces.
-Si el vostre lloc té múltiples adreces públiques haureu de configurar redireccions permanents per a totes excepte aquesta.
-Si el vostre lloc és accessible tant des d\'Internet com des d\'una intranet, utilitzeu aquí l\'adreça pública i configureu el DNS de manera que els usuaris de la intranet puguin utilitzar també l\'adreça pública.
-Si l\'adreça no és correcta, canvieu l\'URL en el vostre navegador per reiniciar la instal·lació amb un altre valor.';
+$string['pathssubwwwroot'] = '<p>L\'adreça web completa on s\'accedirà a Moodle; per exemple, l\'adreça que els usuaris introduiran a la barra d\'adreces del navegador per accedir a Moodle.</p> <p> No és possible accedir a Moodle utilitzant diferents adreces. Si el vostre lloc és accessible a través de diferents adreces, trieu-ne la més fàcil i configureu una redirecció permanent per a cadascuna de les altres adreces.</p> <p>
+Si el vostre lloc és accessible tant des d\'Internet com des d\'una xarxa interna (anomenada de vegades intranet), utilitzeu l\'adreça pública aquí.</p> <p>Si l\'adreça actual no és correcta, canvieu l\'URL a la barra d\'adreces del navegador i reinicieu la instal·lació.';
$string['pathsunsecuredataroot'] = 'La ubicació del dataroot no és segura.';
$string['pathswrongadmindir'] = 'No existeix el directori d\'administració';
$string['phpextension'] = 'Extensió PHP {$a}';
<p>Us cal actualitzar el PHP o traslladar Moodle a un ordinador amb una versió de PHP més recent.<br />(Si esteu utilitzant la versió 5.0.x, alternativament també podríeu tornar enrere a la 4.4.x)</p>';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'Esteu veient aquesta pàgina perquè heu instal·lat amb èxit i heu executat el paquet <strong>{$a->packname} {$a->packversion}</strong>. Felicitacions.';
-$string['welcomep30'] = 'Aquesta versió de <strong>{$a->installername}</strong> inclou les aplicacions necessàries per crear un entorn en el qual funcioni <strong>Moodle</strong>:';
+$string['welcomep30'] = 'Aquesta versió de <strong>{$a->installername}</strong> inclou les aplicacions necessàries per crear un entorn en el qual funcioni <strong>Moodle</strong>, concretament:';
$string['welcomep40'] = 'El paquet inclou també <strong>Moodle {$a->moodlerelease} ({$a->moodleversion})</strong>.';
$string['welcomep50'] = 'L\'ús de totes les aplicacions d\'aquest paquet és governat per les seves llicències respectives. El paquet <strong>{$a->installername}</strong> complet és
<a href="http://www.opensource.org/docs/definition_plain.html">codi font obert</a> i es distribueix
אנא השתמש באפשרות העזרה.';
$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}';
$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>
$string['language'] = 'שפת ממשק';
$string['next'] = 'הבא';
$string['previous'] = 'קודם';
-$string['reload'] = '×\98×¢×\9f מחדש';
+$string['reload'] = '×\98×¢×\99× ×\94 מחדש';
$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>
$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';
$string['categorycurrentuse'] = 'Use this category';
$string['categorydoesnotexist'] = 'This category does not exist';
$string['categoryinfo'] = 'Category info';
-$string['categorymove'] = 'The category \'{$a->name}\' contains {$a->count} questions (some of them may be old, hidden, questions that are still in use in some existing quizzes). Please choose another category to move them to.';
+$string['categorymove'] = 'The category \'{$a->name}\' contains {$a->count} questions (some of them may be old, hidden, questions, or Random questions that are still in use in some existing quizzes). Please choose another category to move them to.';
$string['categorymoveto'] = 'Save in category';
$string['categorynamecantbeblank'] = 'The category name cannot be blank.';
$string['clickflag'] = 'Flag question';
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;
define(function() {
// This module exposes only the global yui instance.
+ /* global Y */
return /** @alias module:core/yui */ Y;
});
function blocks_delete_instances($instanceids) {
global $DB;
- $instances = $DB->get_recordset_list('block_instances', 'id', $instanceids);
- foreach ($instances as $instance) {
- blocks_delete_instance($instance, false, true);
+ $limit = 1000;
+ $count = count($instanceids);
+ $chunks = [$instanceids];
+ if ($count > $limit) {
+ $chunks = array_chunk($instanceids, $limit);
}
- $instances->close();
- $DB->delete_records_list('block_positions', 'blockinstanceid', $instanceids);
- $DB->delete_records_list('block_instances', 'id', $instanceids);
+ // Perform deletion for each chunk.
+ foreach ($chunks as $chunk) {
+ $instances = $DB->get_recordset_list('block_instances', 'id', $chunk);
+ foreach ($instances as $instance) {
+ blocks_delete_instance($instance, false, true);
+ }
+ $instances->close();
+
+ $DB->delete_records_list('block_positions', 'blockinstanceid', $chunk);
+ $DB->delete_records_list('block_instances', 'id', $chunk);
- $preferences = array();
- foreach ($instanceids as $instanceid) {
- $preferences[] = 'block' . $instanceid . 'hidden';
- $preferences[] = 'docked_block_instance_' . $instanceid;
+ $preferences = array();
+ foreach ($chunk as $instanceid) {
+ $preferences[] = 'block' . $instanceid . 'hidden';
+ $preferences[] = 'docked_block_instance_' . $instanceid;
+ }
+ $DB->delete_records_list('user_preferences', 'name', $preferences);
}
- $DB->delete_records_list('user_preferences', 'name', $preferences);
}
/**
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);
}
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);
"notify.js",
"textarea.js",
"autosave.js",
+ "autosave-io.js",
"clean.js",
"commands.js",
"toolbar.js",
--- /dev/null
+// 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]);
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
}
};
form,
optiontype = null,
options = this.get('filepickeroptions'),
- params,
- url;
+ params;
if (!this.get('autosaveEnabled')) {
// Autosave disabled for this instance.
// 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> </p>' || response_json.result === '<p><br> </p>') {
- response_json.result = '';
- }
+ // Check for IE 9 and 10.
+ if (response.result === '<p> </p>' || response.result === '<p><br> </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.
*
};
// 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);
}
}
});
"moodle-core-notification-confirm",
"moodle-editor_atto-rangy",
"handlebars",
- "timers"
+ "timers",
+ "querystring-stringify"
]
},
"moodle-editor_atto-plugin": {
*/
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);
$url = $userauthplugin->edit_profile_url();
if (empty($url)) {
if (empty($course)) {
- $url = new moodle_url('/user/edit.php', array('userid' => $user->id, 'returnto' => 'profile'));
+ $url = new moodle_url('/user/edit.php', array('id' => $user->id, 'returnto' => 'profile'));
} else {
- $url = new moodle_url('/user/edit.php', array('userid' => $user->id, 'course' => $course->id,
+ $url = new moodle_url('/user/edit.php', array('id' => $user->id, 'course' => $course->id,
'returnto' => 'profile'));
}
}
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();
// 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;
}
}
+/**
+ * Remove stale questions from a category.
+ *
+ * While questions should not be left behind when they are not used any more,
+ * it does happen, maybe via restore, or old logic, or uncovered scenarios. When
+ * this happens, the users are unable to delete the question category unless
+ * they move those stale questions to another one category, but to them the
+ * category is empty as it does not contain anything. The purpose of this function
+ * is to detect the questions that may have gone stale and remove them.
+ *
+ * You will typically use this prior to checking if the category contains questions.
+ *
+ * The stale questions (unused and hidden to the user) handled are:
+ * - hidden questions
+ * - random questions
+ *
+ * @param int $categoryid The category ID.
+ */
+function question_remove_stale_questions_from_category($categoryid) {
+ global $DB;
+
+ $select = 'category = :categoryid AND (qtype = :qtype OR hidden = :hidden)';
+ $params = ['categoryid' => $categoryid, 'qtype' => 'random', 'hidden' => 1];
+ $questions = $DB->get_recordset_select("question", $select, $params, '', 'id');
+ foreach ($questions as $question) {
+ // The function question_delete_question does not delete questions in use.
+ question_delete_question($question->id);
+ }
+ $questions->close();
+}
+
/**
* Category is about to be deleted,
* 1/ All questions are deleted for this question category.
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);
+ }
}
/**
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);
}
/**
$criteria = array('category' => $qcat->id);
$this->assertEquals(0, $DB->count_records('question', $criteria));
}
+
+ public function test_question_remove_stale_questions_from_category() {
+ global $DB;
+ $this->resetAfterTest(true);
+ $dg = $this->getDataGenerator();
+ $course = $dg->create_course();
+ $quiz = $dg->create_module('quiz', ['course' => $course->id]);
+
+ $qgen = $dg->get_plugin_generator('core_question');
+ $context = context_system::instance();
+
+ $qcat1 = $qgen->create_question_category(['contextid' => $context->id]);
+ $q1a = $qgen->create_question('shortanswer', null, ['category' => $qcat1->id]); // Will be hidden.
+ $q1b = $qgen->create_question('random', null, ['category' => $qcat1->id]); // Will not be used.
+ $DB->set_field('question', 'hidden', 1, ['id' => $q1a->id]);
+
+ $qcat2 = $qgen->create_question_category(['contextid' => $context->id]);
+ $q2a = $qgen->create_question('shortanswer', null, ['category' => $qcat2->id]); // Will be hidden.
+ $q2b = $qgen->create_question('shortanswer', null, ['category' => $qcat2->id]); // Will be hidden but used.
+ $q2c = $qgen->create_question('random', null, ['category' => $qcat2->id]); // Will not be used.
+ $q2d = $qgen->create_question('random', null, ['category' => $qcat2->id]); // Will be used.
+ $DB->set_field('question', 'hidden', 1, ['id' => $q2a->id]);
+ $DB->set_field('question', 'hidden', 1, ['id' => $q2b->id]);
+ quiz_add_quiz_question($q2b->id, $quiz);
+ quiz_add_quiz_question($q2d->id, $quiz);
+
+ $this->assertEquals(2, $DB->count_records('question', ['category' => $qcat1->id]));
+ $this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
+
+ // Non-existing category, nothing will happen.
+ question_remove_stale_questions_from_category(0);
+ $this->assertEquals(2, $DB->count_records('question', ['category' => $qcat1->id]));
+ $this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
+
+ // First category, should be empty afterwards.
+ question_remove_stale_questions_from_category($qcat1->id);
+ $this->assertEquals(0, $DB->count_records('question', ['category' => $qcat1->id]));
+ $this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
+ $this->assertFalse($DB->record_exists('question', ['id' => $q1a->id]));
+ $this->assertFalse($DB->record_exists('question', ['id' => $q1b->id]));
+
+ // Second category, used questions should be left untouched.
+ question_remove_stale_questions_from_category($qcat2->id);
+ $this->assertEquals(0, $DB->count_records('question', ['category' => $qcat1->id]));
+ $this->assertEquals(2, $DB->count_records('question', ['category' => $qcat2->id]));
+ $this->assertFalse($DB->record_exists('question', ['id' => $q2a->id]));
+ $this->assertTrue($DB->record_exists('question', ['id' => $q2b->id]));
+ $this->assertFalse($DB->record_exists('question', ['id' => $q2c->id]));
+ $this->assertTrue($DB->record_exists('question', ['id' => $q2d->id]));
+ }
}
$this->assertEquals('<div class="no-overflow"><p>:-)</p></div>',
format_text('<p>:-)</p>', FORMAT_HTML, array('overflowdiv' => true)));
}
+
+ /**
+ * Test adding blank target attribute to links
+ *
+ * @dataProvider format_text_blanktarget_testcases
+ * @param string $link The link to add target="_blank" to
+ * @param string $expected The expected filter value
+ */
+ public function test_format_text_blanktarget($link, $expected) {
+ $actual = format_text($link, FORMAT_MOODLE, array('blanktarget' => true, 'filter' => false, 'noclean' => true));
+ $this->assertEquals($expected, $actual);
+ }
+
+ /**
+ * Data provider for the test_format_text_blanktarget testcase
+ *
+ * @return array of testcases
+ */
+ 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>'
+ ],
+ '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>'
+ ]
+ ];
+ }
}
This files describes API changes in core libraries and APIs,
information provided here is intended especially for developers.
+=== 3.2 ===
+
+* New option 'blanktarget' added to format_text. This option adds target="_blank" to links
+
=== 3.1 ===
* Webservice function core_course_search_courses accepts a new parameter 'limittoenrolled' to filter the results
if ($forcedownload) {
$params['forcedownload'] = 1;
}
- $path = rtrim($path, '/');
$url = new moodle_url($urlbase, $params);
$url->set_slashargument($path);
return $url;
* with the class no-overflow before being returned. Default false.
* allowid : If true then id attributes will not be removed, even when
* using htmlpurifier. Default false.
+ * blanktarget : If true all <a> tags will have target="_blank" added unless target is explicitly specified.
* </pre>
*
* @staticvar array $croncache
if (!isset($options['overflowdiv'])) {
$options['overflowdiv'] = false;
}
+ $options['blanktarget'] = !empty($options['blanktarget']);
// Calculate best context.
if (empty($CFG->version) or $CFG->version < 2013051400 or during_initial_install()) {
$text = html_writer::tag('div', $text, array('class' => 'no-overflow'));
}
+ if ($options['blanktarget']) {
+ $domdoc = new DOMDocument();
+ $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;
+ }
+ $link->setAttribute('target', '_blank');
+ if (strpos($link->getAttribute('rel'), 'noreferrer') === false) {
+ $link->setAttribute('rel', trim($link->getAttribute('rel') . ' noreferrer'));
+ }
+ }
+
+ // This regex is nasty and I don't like it. The correct way to solve this is by loading the HTML like so:
+ // $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($domdoc->documentElement)));
+ }
+
return $text;
}
*/
M.core.dock.init = function() {
Y.all(SELECTOR.dockableblock).each(M.core.dock.registerDockableBlock);
+ Y.Global.on(M.core.globalEvents.BLOCK_CONTENT_UPDATED, function(e) {
+ M.core.dock.notifyBlockChange(e.instanceid);
+ }, this);
BODY.delegate('click', M.core.dock.dockBlock, SELECTOR.blockmoveto);
BODY.delegate('key', M.core.dock.dockBlock, SELECTOR.blockmoveto, 'enter');
};
"event-mouseenter",
"event-resize",
"escape",
- "moodle-core-dock-loader"
+ "moodle-core-dock-loader",
+ "moodle-core-event"
]
},
"moodle-core-dock-loader": {
* @param formid {string} Id of form with error.
* @param elementid {string} Id of element with error.
*/
- FORM_ERROR: "form_error"
+ FORM_ERROR: "form_error",
+
+ /**
+ * This event is triggered when the content of a block has changed
+ *
+ * @event "block_content_updated"
+ * @param instanceid ID of the block instance that was updated
+ */
+ BLOCK_CONTENT_UPDATED: "block_content_updated"
};
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)
$options = new stdClass();
$options->para = false;
+ $options->blanktarget = true;
$format = $message->fullmessageformat;
*/
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;
'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;
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,
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'
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.
},
// 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.
* @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,
$this->get_module_name(),
$this->get_instance()->name,
$this->is_blind_marking(),
- $this->get_uniqueid_for_user($userfrom->id));
+ $uniqueid);
}
/**
$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) {
}
$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) {
}
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;
+ }
+
}
/**
$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');
- }
}
/**
$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());
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);
+ }
}
$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);
+ }
}
// Parse the text to clean and filter it.
$options = new stdClass();
$options->para = false;
+ $options->blanktarget = true;
$text = format_text($text, FORMAT_MOODLE, $options, $courseid);
// And now check for special cases.
// Parse the text to clean and filter it.
$options = new stdClass();
$options->para = false;
+ $options->blanktarget = true;
$text = format_text($text, FORMAT_MOODLE, $options, $courseid);
// And now check for special cases.
}
/**
- * @global object
+ * Callback for the "Complete" report - prints the activity summary for the given user
+ *
* @param object $course
* @param object $user
* @param object $mod
* @param object $choice
- * @return string|void
*/
function choice_user_complete($course, $user, $mod, $choice) {
global $DB;
- if ($answer = $DB->get_record('choice_answers', array("choiceid" => $choice->id, "userid" => $user->id))) {
- $result = new stdClass();
- $result->info = "'".format_string(choice_get_option_text($choice, $answer->optionid))."'";
- $result->time = $answer->timemodified;
- echo get_string("answered", "choice").": $result->info. ".get_string("updated", '', userdate($result->time));
+ if ($answers = $DB->get_records('choice_answers', array("choiceid" => $choice->id, "userid" => $user->id))) {
+ $info = [];
+ foreach ($answers as $answer) {
+ $info[] = "'" . format_string(choice_get_option_text($choice, $answer->optionid)) . "'";
+ }
+ core_collator::asort($info);
+ echo get_string("answered", "choice") . ": ". join(', ', $info) . ". " .
+ get_string("updated", '', userdate($answer->timemodified));
} else {
print_string("notanswered", "choice");
}
--- /dev/null
+<?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 {
+}
var value = selectlist.options[selectlist.selectedIndex].value;
var editorname = 'template';
if (typeof tinyMCE == 'undefined') {
- var element = document.getElementsByName(editorname)[0];
- // For inserting when in normal textareas
- insertAtCursor(element, value);
+ if (document.execCommand('insertText')) {
+ document.execCommand('insertText', false, value);
+ } else {
+ var element = document.getElementsByName(editorname)[0];
+ // For inserting when in normal textareas
+ insertAtCursor(element, value);
+ }
} else {
tinyMCE.execInstanceCommand(editorname, 'mceInsertContent', false, value);
}
$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';
// 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);
$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
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;
}
"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;
}
}
/**
- * @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
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');
}
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');
$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);
// 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;
}
}
}
* @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();
}
/**
if ($groupstudents) {
list($usql, $uparams) = $DB->get_in_or_equal($groupstudents);
$where .= " AND quiza.userid $usql";
- $params += $uparams;
+ $params = array_merge($params, $uparams);
}
- $toregrade = $DB->get_records_sql("
+ $toregrade = $DB->get_recordset_sql("
SELECT quiza.uniqueid, qqr.slot
FROM {quiz_attempts} quiza
JOIN {quiz_overview_regrades} qqr ON qqr.questionusageid = quiza.uniqueid
WHERE $where", $params);
- if (!$toregrade) {
- return;
- }
-
$attemptquestions = array();
foreach ($toregrade as $row) {
$attemptquestions[$row->uniqueid][] = $row->slot;
}
+ $toregrade->close();
+
+ if (!$attemptquestions) {
+ return;
+ }
+
$attempts = $DB->get_records_list('quiz_attempts', 'uniqueid',
array_keys($attemptquestions));
$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);
+ }
+
}
}
$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) {
$workshop->phaseswitchassessment = (int)!empty($workshop->phaseswitchassessment);
$workshop->evaluation = 'best';
+ if (isset($workshop->gradinggradepass)) {
+ $workshop->gradinggradepass = unformat_float($workshop->gradinggradepass);
+ }
+
+ if (isset($workshop->submissiongradepass)) {
+ $workshop->submissiongradepass = unformat_float($workshop->submissiongradepass);
+ }
+
if (isset($workshop->submissionfiletypes)) {
$workshop->submissionfiletypes = workshop::clean_file_extensions($workshop->submissionfiletypes);
}
$workshop->latesubmissions = (int)!empty($workshop->latesubmissions);
$workshop->phaseswitchassessment = (int)!empty($workshop->phaseswitchassessment);
+ if (isset($workshop->gradinggradepass)) {
+ $workshop->gradinggradepass = unformat_float($workshop->gradinggradepass);
+ }
+
+ if (isset($workshop->submissiongradepass)) {
+ $workshop->submissiongradepass = unformat_float($workshop->submissiongradepass);
+ }
+
if (isset($workshop->submissionfiletypes)) {
$workshop->submissionfiletypes = workshop::clean_file_extensions($workshop->submissionfiletypes);
}
$mform->addElement('text', 'submissiongradepass', get_string('gradetopasssubmission', 'workshop'));
$mform->addHelpButton('submissiongradepass', 'gradepass', 'grades');
$mform->setDefault('submissiongradepass', '');
- $mform->setType('submissiongradepass', PARAM_FLOAT);
- $mform->addRule('submissiongradepass', null, 'numeric', null, 'client');
+ $mform->setType('submissiongradepass', PARAM_RAW);
$label = get_string('gradinggrade', 'workshop');
$mform->addGroup(array(
$mform->addElement('text', 'gradinggradepass', get_string('gradetopassgrading', 'workshop'));
$mform->addHelpButton('gradinggradepass', 'gradepass', 'grades');
$mform->setDefault('gradinggradepass', '');
- $mform->setType('gradinggradepass', PARAM_FLOAT);
- $mform->addRule('gradinggradepass', null, 'numeric', null, 'client');
+ $mform->setType('gradinggradepass', PARAM_RAW);
$options = array();
for ($i = 5; $i >= 0; $i--) {
}
}
- if ($data['submissiongradepass'] > $data['grade']) {
- $errors['submissiongradepass'] = get_string('gradepassgreaterthangrade', 'grades', $data['grade']);
+ // Check that the submission grade pass is a valid number.
+ if (isset($data['submissiongradepass'])) {
+ $submissiongradefloat = unformat_float($data['submissiongradepass'], true);
+ if ($submissiongradefloat === false || $submissiongradefloat === null) {
+ $errors['submissiongradepass'] = get_string('err_numeric', 'form');
+ } else {
+ if ($submissiongradefloat > $data['grade']) {
+ $errors['submissiongradepass'] = get_string('gradepassgreaterthangrade', 'grades', $data['grade']);
+ }
+ }
}
- if ($data['gradinggradepass'] > $data['gradinggrade']) {
- $errors['gradinggradepass'] = get_string('gradepassgreaterthangrade', 'grades', $data['gradinggrade']);
+
+ // Check that the grade pass is a valid number.
+ if (isset($data['gradinggradepass'])) {
+ $gradepassfloat = unformat_float($data['gradinggradepass'], true);
+ if ($gradepassfloat === false || $gradepassfloat === null) {
+ $errors['gradinggradepass'] = get_string('err_numeric', 'form');
+ } else {
+ if ($gradepassfloat > $data['gradinggrade']) {
+ $errors['gradinggradepass'] = get_string('gradepassgreaterthangrade', 'grades', $data['gradinggrade']);
+ }
+ }
}
return $errors;
function my_reset_page_for_all_users($private = MY_PAGE_PRIVATE, $pagetype = 'my-index') {
global $DB;
+ // This may take a while. Raise the execution time limit.
+ core_php_time_limit::raise();
+
// Find all the user pages.
$where = 'userid IS NOT NULL AND private = :private';
$params = array('private' => $private);
$pages = $DB->get_recordset_select('my_pages', $where, $params, 'id, userid');
- $pageids = array();
$blockids = array();
foreach ($pages as $page) {
- $pageids[] = $page->id;
$usercontext = context_user::instance($page->userid);
// Find all block instances in that page.
- $blocks = $DB->get_recordset('block_instances', array('parentcontextid' => $usercontext->id,
- 'pagetypepattern' => $pagetype), '', 'id, subpagepattern');
- foreach ($blocks as $block) {
- if (is_null($block->subpagepattern) || $block->subpagepattern == $page->id) {
- $blockids[] = $block->id;
- }
+ $blockswhere = 'parentcontextid = :parentcontextid AND
+ pagetypepattern = :pagetypepattern AND
+ (subpagepattern IS NULL OR subpagepattern = :subpagepattern)';
+ $blockswhereparams = [
+ 'parentcontextid' => $usercontext->id,
+ 'pagetypepattern' => $pagetype,
+ 'subpagepattern' => $page->id
+ ];
+ if ($pageblockids = $DB->get_fieldset_select('block_instances', 'id', $blockswhere, $blockswhereparams)) {
+ $blockids = array_merge($blockids, $pageblockids);
}
- $blocks->close();
}
$pages->close();
}
// Finally delete the pages.
- if (!empty($pageids)) {
- list($insql, $inparams) = $DB->get_in_or_equal($pageids);
- $DB->delete_records_select('my_pages', "id $insql", $pageids);
+ if (!empty($pages)) {
+ $DB->delete_records_select('my_pages', $where, $params);
}
// We should be good to go now.
"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",