Compare commits
196 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8c84b4af1 | |||
| 5ec01a7226 | |||
| f878e05d62 | |||
| aa0e2cffdd | |||
| c18b59808c | |||
| a0d8c025f7 | |||
| 137d311fd1 | |||
| 57f20b6cb3 | |||
| a10506b8d7 | |||
| 28b8fc9896 | |||
| f55b829f44 | |||
| 836b2c23a2 | |||
| 0068701bc6 | |||
| 83a978d1cd | |||
| d05795db8e | |||
| 629a7066be | |||
| 058b11e57b | |||
| aa230649d9 | |||
| f8cce391b2 | |||
| a7d476c35f | |||
| 5425fb3d79 | |||
| 587d737690 | |||
| 31b68a2b5d | |||
| a48f00d940 | |||
| f7da739ea0 | |||
| ebb346cd8e | |||
| b39d8d5027 | |||
| cf48f359ad | |||
| 8ac29c9422 | |||
| 7f3617860c | |||
| b35f70ce14 | |||
| 77d0c0caa9 | |||
| c2193308a6 | |||
| 9456911fe9 | |||
| bd2c8fdd54 | |||
| 234d81e39b | |||
| f687e02dec | |||
| 72c3038925 | |||
| d393030c58 | |||
| d68ea84924 | |||
| 85825dcc55 | |||
| 80f152296c | |||
| 48aa9aae56 | |||
| 025065daf5 | |||
| 49be0febdc | |||
| 73f57aab98 | |||
| 457f0e9384 | |||
| 4b15c803e2 | |||
| 50aff515b7 | |||
| 5b33398f07 | |||
| 47568f31c5 | |||
| eba83406da | |||
| 137c6f36bb | |||
| d9043b324d | |||
| e57e560385 | |||
| eebd5742b6 | |||
| 0837a24c05 | |||
| f2e48b39f6 | |||
| 5060c53176 | |||
| 3f392e934e | |||
| c1e4679ed5 | |||
| 9254b7e75a | |||
| 1d609ca0ee | |||
| 61fabd3e73 | |||
| e8d03fdae4 | |||
| c8922328b2 | |||
| 19635ef720 | |||
| 9d5a4746ab | |||
| e4406472f0 | |||
| cf2f67272a | |||
| 2ebac5604c | |||
| c5f48698da | |||
| c2a043f50d | |||
| 001c78285f | |||
| af90bb572e | |||
| 882a660f7e | |||
| adc1de569d | |||
| 607716f206 | |||
| 5358ce28ee | |||
| fef8dda150 | |||
| b049731dfb | |||
| 5277dca108 | |||
| 4be4d0d489 | |||
| 3d0bcb7172 | |||
| 7dac3707fd | |||
| 037a7d5d6c | |||
| f5587cf881 | |||
| a18c0b7c8b | |||
| 097a879e1a | |||
| 58acb96ee6 | |||
| 1901cd3fdf | |||
| 2c9a5c009f | |||
| b564a40d74 | |||
| 179bfdf5c5 | |||
| eb975467bd | |||
| 5efbd6e0e1 | |||
| fa20b1790f | |||
| 7d579dd4bb | |||
| 6b0661b9ea | |||
| afd57ca9a0 | |||
| c14e75a017 | |||
| 318e2344c0 | |||
| 17ed404ccc | |||
| 5b54a87858 | |||
| ea49be1e61 | |||
| 0f3d3b2d77 | |||
| dee41e0648 | |||
| c11b1c6b3f | |||
| e2362b0a7a | |||
| d63721cb15 | |||
| 66b1733bda | |||
| 81821e6833 | |||
| 965a5fdb27 | |||
| 6aa2828161 | |||
| d46a37ebd4 | |||
| 8c358aefd9 | |||
| 40aceb0256 | |||
| f64240ce94 | |||
| 10c7926c0f | |||
| 8a26315686 | |||
| 0e8d4d8e2a | |||
| 5ac11701ac | |||
| dc65317ba6 | |||
| f0ae7c4343 | |||
| b52c1855fa | |||
| 6e1e63cc6b | |||
| 50ae525be9 | |||
| a700d119df | |||
| 9ccd794b36 | |||
| 49673645ea | |||
| e6316e6b9b | |||
| 5003afbe48 | |||
| 7e1bbaaf20 | |||
| d1111bc3e8 | |||
| 8de03684ac | |||
| 2db07c4d0a | |||
| 3ac3d942dd | |||
| 84308c7dfc | |||
| 24a0e76ebf | |||
| 2444b2b787 | |||
| b704fa7dda | |||
| 9d3d193f29 | |||
| 43c7557e84 | |||
| da74ac4987 | |||
| 1e5bc3a393 | |||
| 7c20cd1b81 | |||
| edf1a1ac8c | |||
| 5a2acac1ed | |||
| 719c95009e | |||
| 860b4b941a | |||
| 26ed48ec40 | |||
| 58ba1a9448 | |||
| 40d2b4de39 | |||
| bce1d7ff85 | |||
| 3f439ff9d4 | |||
| 0c34355c85 | |||
| ab118d48f5 | |||
| 6df3213c9b | |||
| 6dbd0e938f | |||
| 8889f6f26f | |||
| 36aa09726b | |||
| 196c2ae7a5 | |||
| 4d217e3a1a | |||
| 09e3db1aa6 | |||
| 8815ecb3fd | |||
| 8482210209 | |||
| 281ef7ef8d | |||
| 6970580b4c | |||
| 652633b467 | |||
| 5a7963d9f5 | |||
| 56ff5e78f1 | |||
| e31db928b5 | |||
| 03fcd81892 | |||
| 05a41b6e32 | |||
| b3867e7422 | |||
| 8c298f14bb | |||
| 58bd2ed8e4 | |||
| 36ca488c06 | |||
| bca28b03a9 | |||
| a6216bb890 | |||
| 94b14df11d | |||
| 0dc60026c8 | |||
| 63a1ec81c4 | |||
| 150fc0837a | |||
| 72958879c6 | |||
| d8d560b7fe | |||
| 917dd49db2 | |||
| 80691cf5a3 | |||
| e20720ce0e | |||
| f976349b95 | |||
| 312a166f1e | |||
| f16ca8fa7a | |||
| 846a8b6568 | |||
| 692d3090fa | |||
| 8092eb95c4 | |||
| 2b864ceec3 |
@@ -1,8 +1,12 @@
|
||||
{
|
||||
'plugins': [
|
||||
'babel',
|
||||
'@babel',
|
||||
'promise',
|
||||
'jsdoc',
|
||||
'jsdoc'
|
||||
],
|
||||
'extends': [
|
||||
'eslint:recommended',
|
||||
'plugin:promise/recommended'
|
||||
],
|
||||
'env': {
|
||||
'browser': true,
|
||||
@@ -16,42 +20,19 @@
|
||||
'M': true,
|
||||
'Y': true
|
||||
},
|
||||
// Warn about unused eslint-disable statements.
|
||||
'reportUnusedDisableDirectives': true,
|
||||
'rules': {
|
||||
// See http://eslint.org/docs/rules/ for all rules and explanations of all
|
||||
// rules.
|
||||
|
||||
// === Possible Errors ===
|
||||
'comma-dangle': 'off',
|
||||
'no-compare-neg-zero': 'error',
|
||||
'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',
|
||||
'no-empty': 'warn',
|
||||
'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-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',
|
||||
'no-unsafe-negation': 'error',
|
||||
'use-isnan': 'error',
|
||||
'valid-jsdoc': ['warn', { 'requireReturn': false, 'requireParamDescription': false, 'requireReturnDescription': false }],
|
||||
'valid-typeof': 'error',
|
||||
|
||||
// === Best Practices ===
|
||||
// (these mostly match our jshint config)
|
||||
@@ -63,15 +44,12 @@
|
||||
'dot-notation': 'warn',
|
||||
'no-alert': 'warn',
|
||||
'no-caller': 'error',
|
||||
'no-case-declarations': 'error',
|
||||
'no-div-regex': 'error',
|
||||
'no-empty-pattern': 'error',
|
||||
'no-empty-function': 'warn',
|
||||
'no-eq-null': 'error',
|
||||
'no-eval': 'error',
|
||||
'no-extend-native': 'error',
|
||||
'no-extra-bind': 'warn',
|
||||
'no-fallthrough': 'error',
|
||||
'no-floating-decimal': 'warn',
|
||||
'no-global-assign': 'warn',
|
||||
'no-implied-eval': 'error',
|
||||
@@ -83,28 +61,22 @@
|
||||
'no-multi-str': 'error',
|
||||
'no-new-func': 'error',
|
||||
'no-new-wrappers': 'error',
|
||||
'no-octal': 'error',
|
||||
'no-octal-escape': 'error',
|
||||
'no-proto': 'error',
|
||||
'no-redeclare': 'warn',
|
||||
'no-restricted-globals': ['error', { 'name': 'Notification' }],
|
||||
'no-return-assign': 'error',
|
||||
'no-script-url': 'error',
|
||||
'no-self-assign': 'error',
|
||||
'no-self-compare': 'error',
|
||||
'no-sequences': 'warn',
|
||||
'no-throw-literal': 'warn',
|
||||
'no-unmodified-loop-condition': 'error',
|
||||
'no-unused-expressions': 'error',
|
||||
'no-unused-labels': 'error',
|
||||
'no-useless-call': 'warn',
|
||||
'no-useless-escape': 'warn',
|
||||
'no-with': 'error',
|
||||
'wrap-iife': ['error', 'any'],
|
||||
|
||||
// === Variables ===
|
||||
'no-delete-var': 'error',
|
||||
'no-undef': 'error',
|
||||
'no-undef-init': 'error',
|
||||
'no-unused-vars': ['error', { 'caughtErrors': 'none' }],
|
||||
|
||||
@@ -137,8 +109,6 @@
|
||||
'max-statements-per-line': ['warn', { max: 2 }],
|
||||
'new-cap': ['warn', { 'properties': false }],
|
||||
'new-parens': 'warn',
|
||||
'newline-after-var': 'off',
|
||||
'newline-before-return': 'off',
|
||||
'newline-per-chained-call': 'off',
|
||||
'no-array-constructor': 'off',
|
||||
'no-bitwise': 'error',
|
||||
@@ -146,7 +116,6 @@
|
||||
'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',
|
||||
@@ -168,7 +137,6 @@
|
||||
'padded-blocks': 'off',
|
||||
'quote-props': ['warn', 'as-needed', {'unnecessary': false, 'keywords': true, 'numbers': true}],
|
||||
'quotes': 'off',
|
||||
'require-jsdoc': 'warn',
|
||||
'semi': 'error',
|
||||
'semi-spacing': ['warn', {'before': false, 'after': true}],
|
||||
'sort-vars': 'off',
|
||||
@@ -181,21 +149,19 @@
|
||||
'unicode-bom': 'error',
|
||||
'wrap-regex': 'off',
|
||||
|
||||
// === Promises ===
|
||||
// === Promises (override plugin:promise/recommended) ===
|
||||
'promise/always-return': 'warn',
|
||||
'promise/no-return-wrap': 'warn',
|
||||
'promise/param-names': 'warn',
|
||||
'promise/catch-or-return': ['warn', {terminationMethod: ['catch', 'fail']}],
|
||||
'promise/no-native': 'warn',
|
||||
'promise/no-promise-in-callback': 'warn',
|
||||
'promise/no-callback-in-promise': 'warn',
|
||||
'promise/avoid-new': 'warn',
|
||||
|
||||
// === Deprecations ===
|
||||
"no-restricted-properties": ['warn', {
|
||||
'object': 'M',
|
||||
'property': 'str',
|
||||
'message': 'Use AMD module "core/str" or M.util.get_string()'
|
||||
'message': 'Use "core/str" module or M.util.get_string()'
|
||||
}],
|
||||
},
|
||||
overrides: [
|
||||
@@ -235,7 +201,6 @@
|
||||
parser: '@babel/eslint-parser',
|
||||
// Check AMD with some slightly stricter rules.
|
||||
rules: {
|
||||
'no-unused-vars': 'error',
|
||||
'no-implicit-globals': 'error',
|
||||
// Disable all of the rules that have babel versions.
|
||||
'new-cap': 'off',
|
||||
@@ -247,65 +212,37 @@
|
||||
'semi': 'off',
|
||||
'no-unused-expressions': 'off',
|
||||
// Enable all of the babel version of these rules.
|
||||
'babel/new-cap': ['warn', { 'properties': false }],
|
||||
'@babel/new-cap': ['warn', { 'properties': false }],
|
||||
// Not using this rule for the time being because it isn't
|
||||
// compatible with jQuery and ES6.
|
||||
'babel/no-invalid-this': 'off',
|
||||
'babel/object-curly-spacing': 'warn',
|
||||
// This is off in the original style int.
|
||||
'babel/quotes': 'off',
|
||||
'babel/semi': 'error',
|
||||
'babel/no-unused-expressions': 'error',
|
||||
'@babel/no-invalid-this': 'off',
|
||||
'@babel/object-curly-spacing': 'warn',
|
||||
'@babel/semi': 'error',
|
||||
'@babel/no-unused-expressions': 'error',
|
||||
// === Promises ===
|
||||
// We have Promise now that we're using ES6.
|
||||
'promise/no-native': 'off',
|
||||
'promise/avoid-new': 'off',
|
||||
|
||||
// === JSDocs ===
|
||||
"jsdoc/check-access": [
|
||||
'error',
|
||||
],
|
||||
"jsdoc/check-alignment": 1, // Recommended.
|
||||
"jsdoc/check-param-names": [
|
||||
'error',
|
||||
],
|
||||
"jsdoc/check-property-names": [
|
||||
'error',
|
||||
],
|
||||
"jsdoc/empty-tags": [
|
||||
'error',
|
||||
],
|
||||
"jsdoc/implements-on-classes": [
|
||||
'error',
|
||||
],
|
||||
"jsdoc/multiline-blocks": [
|
||||
'error',
|
||||
],
|
||||
"jsdoc/require-jsdoc": [
|
||||
'error',
|
||||
],
|
||||
"jsdoc/require-param": [
|
||||
'error',
|
||||
],
|
||||
"jsdoc/require-param-name": [
|
||||
'error',
|
||||
],
|
||||
"jsdoc/require-param-type": [
|
||||
'error',
|
||||
],
|
||||
"jsdoc/require-property": [
|
||||
'error',
|
||||
],
|
||||
"jsdoc/require-property-name": [
|
||||
'error',
|
||||
],
|
||||
"jsdoc/require-property-type": [
|
||||
'error',
|
||||
],
|
||||
'jsdoc/check-access': 'error',
|
||||
'jsdoc/check-alignment': 1, // Recommended.
|
||||
'jsdoc/check-param-names': 'error',
|
||||
'jsdoc/check-property-names': 'error',
|
||||
'jsdoc/empty-tags': 'error',
|
||||
'jsdoc/implements-on-classes': 'error',
|
||||
'jsdoc/multiline-blocks': 'error',
|
||||
'jsdoc/require-jsdoc': 'error',
|
||||
'jsdoc/require-param': 'error',
|
||||
'jsdoc/require-param-name': 'error',
|
||||
'jsdoc/require-param-type': 'error',
|
||||
'jsdoc/require-property': 'error',
|
||||
'jsdoc/require-property-name': 'error',
|
||||
'jsdoc/require-property-type': 'error'
|
||||
},
|
||||
parserOptions: {
|
||||
'sourceType': 'module',
|
||||
'requireConfigFile': false,
|
||||
'requireConfigFile': false
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Function to generate the destination for the uglify task
|
||||
* Function to generate the destination for the minification task
|
||||
* (e.g. build/file.min.js). This function will be passed to
|
||||
* the rename property of files array when building dynamically:
|
||||
* http://gruntjs.com/configuring-tasks#building-the-files-object-dynamically
|
||||
@@ -60,7 +60,6 @@ module.exports = grunt => {
|
||||
grunt.registerTask('js', ['amd', 'yui']);
|
||||
|
||||
// Register NPM tasks.
|
||||
grunt.loadNpmTasks('grunt-contrib-uglify');
|
||||
grunt.loadNpmTasks('grunt-contrib-watch');
|
||||
grunt.loadNpmTasks('grunt-rollup');
|
||||
|
||||
@@ -104,6 +103,7 @@ module.exports = grunt => {
|
||||
// The queue runner will run the next `size` items in the queue.
|
||||
const runQueue = (size = 1) => {
|
||||
queue.splice(0, size).forEach(resolve => {
|
||||
grunt.log.debug(`Item resolved. Kicking off next one.`);
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
@@ -113,15 +113,17 @@ module.exports = grunt => {
|
||||
|
||||
// The options hook is run in parallel.
|
||||
// We can return an unresolved Promise which is queued for later resolution.
|
||||
options: async() => {
|
||||
options: async(options) => {
|
||||
return new Promise(resolve => {
|
||||
queue.push(resolve);
|
||||
startQueue();
|
||||
return options;
|
||||
});
|
||||
},
|
||||
|
||||
// When an item in the queue completes, start the next item in the queue.
|
||||
buildEnd: () => {
|
||||
generateBundle: (options, bundle) => {
|
||||
grunt.log.debug(`Finished output phase for ${Object.keys(bundle).join(', ')}`);
|
||||
runQueue();
|
||||
},
|
||||
};
|
||||
@@ -151,24 +153,10 @@ module.exports = grunt => {
|
||||
//
|
||||
// It also adds the Moodle plugin name to the AMD module definition
|
||||
// so that it can be imported as expected in other modules.
|
||||
path.resolve('.grunt/babel-plugin-add-module-to-define.js'),
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
'@babel/plugin-syntax-import-meta',
|
||||
['@babel/plugin-proposal-class-properties', {'loose': false}],
|
||||
'@babel/plugin-proposal-json-strings'
|
||||
path.resolve('.grunt/babel-plugin-add-module-to-define.js')
|
||||
],
|
||||
presets: [
|
||||
['@babel/preset-env', {
|
||||
targets: {
|
||||
browsers: [
|
||||
">0.3%",
|
||||
"last 2 versions",
|
||||
"not ie >= 0",
|
||||
"not op_mini all",
|
||||
"not Opera > 0",
|
||||
"not dead"
|
||||
]
|
||||
},
|
||||
modules: false,
|
||||
useBuiltIns: false
|
||||
}]
|
||||
|
||||
+28
-12
@@ -20,17 +20,33 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
module.exports = grunt => {
|
||||
// Project configuration.
|
||||
grunt.config.merge({
|
||||
jsdoc: {
|
||||
dist: {
|
||||
options: {
|
||||
configure: ".grunt/jsdoc/jsdoc.conf.js",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
module.exports = (grunt) => {
|
||||
const path = require('path');
|
||||
|
||||
grunt.loadNpmTasks('grunt-jsdoc');
|
||||
grunt.registerTask('jsdoc', 'Generate JavaScript documentation using jsdoc', function() {
|
||||
const done = this.async();
|
||||
const configuration = path.resolve('.grunt/jsdoc/jsdoc.conf.js');
|
||||
|
||||
grunt.util.spawn({
|
||||
cmd: 'jsdoc',
|
||||
args: [
|
||||
'--configure',
|
||||
configuration,
|
||||
]
|
||||
}, function(error, result, code) {
|
||||
if (result.stdout) {
|
||||
grunt.log.write(result.stdout);
|
||||
}
|
||||
|
||||
if (result.stderr) {
|
||||
grunt.log.error(result.stderr);
|
||||
}
|
||||
if (error) {
|
||||
grunt.fail.fatal(`JSDoc failed with error code ${code}`);
|
||||
} else {
|
||||
grunt.log.write('JSDoc completed successfully'.green);
|
||||
}
|
||||
done();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@ module.exports = grunt => {
|
||||
// Use a fully-qualified path.
|
||||
src: files,
|
||||
options: {
|
||||
quietDeprecationWarnings: true,
|
||||
configOverrides: {
|
||||
rules: {
|
||||
// These rules have to be disabled in .stylelintrc for scss compat.
|
||||
@@ -45,7 +46,10 @@ module.exports = grunt => {
|
||||
return {
|
||||
stylelint: {
|
||||
scss: {
|
||||
options: {syntax: 'scss'},
|
||||
options: {
|
||||
quietDeprecationWarnings: true,
|
||||
customSyntax: 'postcss-scss',
|
||||
},
|
||||
src: files,
|
||||
},
|
||||
},
|
||||
|
||||
+16
-4
@@ -1,9 +1,14 @@
|
||||
{
|
||||
"customSyntax": "postcss-scss",
|
||||
"plugins": [
|
||||
"stylelint-csstree-validator"
|
||||
],
|
||||
"rules": {
|
||||
"csstree/validator": true,
|
||||
"csstree/validator": {
|
||||
"syntaxExtensions": [
|
||||
"sass"
|
||||
]
|
||||
},
|
||||
"at-rule-empty-line-before": [ "always",
|
||||
{"except": [ "blockless-after-blockless"], ignore: ["after-comment", "inside-block"]}
|
||||
],
|
||||
@@ -45,7 +50,7 @@
|
||||
"function-name-case": "lower",
|
||||
"function-parentheses-newline-inside": "always-multi-line",
|
||||
"function-parentheses-space-inside": "never-single-line",
|
||||
"function-url-scheme-blacklist": ["data"],
|
||||
"function-url-scheme-disallowed-list": ["data"],
|
||||
"function-whitespace-after": "always",
|
||||
"indentation": 4,
|
||||
"keyframe-declaration-no-important": true,
|
||||
@@ -84,12 +89,19 @@
|
||||
"selector-type-no-unknown": true,
|
||||
"string-no-newline": true,
|
||||
"time-min-milliseconds": 100,
|
||||
"unit-blacklist": ["pt"],
|
||||
"unit-disallowed-list": ["pt"],
|
||||
"unit-case": "lower",
|
||||
"unit-no-unknown": true,
|
||||
"value-keyword-case": ["lower", {"ignoreKeywords": ["/(@|$)/"]}],
|
||||
"value-list-comma-newline-after": "always-multi-line",
|
||||
"value-list-comma-space-after": "always-single-line",
|
||||
"value-list-comma-space-before": "never",
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/yui/**/*.css"],
|
||||
"rules": {
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3923,6 +3923,8 @@
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_xmlrpc_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_oracle_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="4.2" requires="3.11.8">
|
||||
@@ -3939,6 +3941,7 @@
|
||||
<VENDOR name="oracle" version="19" />
|
||||
</DATABASE>
|
||||
<PHP version="8.0.0" level="required">
|
||||
<RESTRICT function="restrict_php_version_83" message="unsupportedphpversion83" />
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
@@ -4112,6 +4115,8 @@
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mod_assignment" level="required">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_oracle_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="4.3" requires="3.11.8">
|
||||
@@ -4128,6 +4133,7 @@
|
||||
<VENDOR name="oracle" version="19" />
|
||||
</DATABASE>
|
||||
<PHP version="8.0.0" level="required">
|
||||
<RESTRICT function="restrict_php_version_83" message="unsupportedphpversion83" />
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
@@ -4303,6 +4309,8 @@
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_db_prefix_length" level="required">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_oracle_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="4.4" requires="4.1.2">
|
||||
@@ -4494,6 +4502,8 @@
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_db_prefix_length" level="required">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_oracle_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
</COMPATIBILITY_MATRIX>
|
||||
|
||||
@@ -64,7 +64,7 @@ class delete extends base {
|
||||
* Delete the DB preset
|
||||
*/
|
||||
public function execute(): void {
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
$this->manager->delete_preset($this->id);
|
||||
|
||||
|
||||
@@ -52,8 +52,6 @@ class export extends base {
|
||||
* Stores a preset into the DB.
|
||||
*/
|
||||
public function execute(): void {
|
||||
confirm_sesskey();
|
||||
|
||||
$url = new \moodle_url('/admin/tool/admin_presets/index.php', ['action' => 'export', 'mode' => 'execute']);
|
||||
$this->moodleform = new export_form($url);
|
||||
|
||||
@@ -87,7 +85,7 @@ class export extends base {
|
||||
* @throws xml_writer_exception
|
||||
*/
|
||||
public function download_xml(): void {
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
list($xmlstr, $filename) = $this->manager->download_preset($this->id);
|
||||
|
||||
|
||||
@@ -40,8 +40,6 @@ class import extends base {
|
||||
* Imports the xmlfile into DB
|
||||
*/
|
||||
public function execute(): void {
|
||||
confirm_sesskey();
|
||||
|
||||
$url = new \moodle_url('/admin/tool/admin_presets/index.php', ['action' => 'import', 'mode' => 'execute']);
|
||||
$this->moodleform = new import_form($url);
|
||||
|
||||
|
||||
@@ -38,8 +38,6 @@ class load extends base {
|
||||
public function execute(): void {
|
||||
global $OUTPUT;
|
||||
|
||||
confirm_sesskey();
|
||||
|
||||
$url = new \moodle_url('/admin/tool/admin_presets/index.php', ['action' => 'load', 'mode' => 'execute']);
|
||||
$this->moodleform = new load_form($url);
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ class rollback extends base {
|
||||
public function execute(): void {
|
||||
global $OUTPUT;
|
||||
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
list($presetapp, $rollback, $failures) = $this->manager->revert_preset($this->id);
|
||||
|
||||
|
||||
@@ -97,28 +97,28 @@ if ($onlycli === false) {
|
||||
switch ($action) {
|
||||
|
||||
case 'enable':
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
$model->enable();
|
||||
redirect($returnurl);
|
||||
break;
|
||||
|
||||
case 'disable':
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
$model->update(0, false, false);
|
||||
redirect($returnurl);
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
$model->delete();
|
||||
redirect($returnurl);
|
||||
break;
|
||||
|
||||
case 'edit':
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
$invalidcurrenttimesplitting = $model->invalid_timesplitting_selected();
|
||||
$potentialtimesplittings = $model->get_potential_timesplittings();
|
||||
@@ -183,7 +183,7 @@ switch ($action) {
|
||||
break;
|
||||
|
||||
case 'evaluate':
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
if ($model->is_static()) {
|
||||
throw new moodle_exception('errornostaticevaluate', 'tool_analytics');
|
||||
@@ -226,7 +226,7 @@ switch ($action) {
|
||||
break;
|
||||
|
||||
case 'scheduledanalysis':
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
if ($onlycli) {
|
||||
throw new moodle_exception('erroronlycli', 'tool_analytics');
|
||||
@@ -292,7 +292,7 @@ switch ($action) {
|
||||
break;
|
||||
|
||||
case 'clear':
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
$model->clear();
|
||||
redirect($returnurl);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -71,14 +71,12 @@ define(['jquery'], function($) {
|
||||
return null;
|
||||
};
|
||||
|
||||
// eslint-disable-line valid-jsdoc
|
||||
/**
|
||||
* Return the type of the module.
|
||||
*
|
||||
* @return {String}
|
||||
* @method getType
|
||||
*/
|
||||
// eslint-enable-line valid-jsdoc
|
||||
Rule.prototype.getType = function() {
|
||||
throw new Error('Not implemented');
|
||||
};
|
||||
|
||||
@@ -419,13 +419,13 @@ class api {
|
||||
public static function get_qrlogin_key(stdClass $mobilesettings) {
|
||||
global $USER;
|
||||
// Delete previous keys.
|
||||
delete_user_key('tool_mobile', $USER->id);
|
||||
delete_user_key('tool_mobile/qrlogin', $USER->id);
|
||||
|
||||
// Create a new key.
|
||||
$iprestriction = !empty($mobilesettings->qrsameipcheck) ? getremoteaddr(null) : null;
|
||||
$qrkeyttl = !empty($mobilesettings->qrkeyttl) ? $mobilesettings->qrkeyttl : self::LOGIN_QR_KEY_TTL;
|
||||
$validuntil = time() + $qrkeyttl;
|
||||
return create_user_key('tool_mobile', $USER->id, null, $iprestriction, $validuntil);
|
||||
return create_user_key('tool_mobile/qrlogin', $USER->id, null, $iprestriction, $validuntil);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace tool_mobile;
|
||||
|
||||
use core\session\utility\cookie_helper;
|
||||
use core\event\user_loggedin;
|
||||
|
||||
/**
|
||||
* Event handler for tool_mobile.
|
||||
*
|
||||
* @package tool_mobile
|
||||
* @copyright 2024 Juan Leyva
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class event_handler {
|
||||
|
||||
/**
|
||||
* Allows the plugin to augment Set-Cookie headers when the user_loggedin event is fired as part of complete_user_login() calls.
|
||||
*
|
||||
* @param user_loggedin $event the event
|
||||
* @return void
|
||||
*/
|
||||
public static function handle_user_loggedin(user_loggedin $event): void {
|
||||
global $CFG;
|
||||
|
||||
// Set Partitioned and Secure attributes to the MoodleSession cookie if the user is using the Moodle app.
|
||||
if (\core_useragent::is_moodle_app()) {
|
||||
cookie_helper::add_attributes_to_cookie_response_header('MoodleSession'.$CFG->sessioncookie, ['Secure', 'Partitioned']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -649,8 +649,8 @@ class external extends external_api {
|
||||
api::check_autologin_prerequisites($params['userid']); // Checks https, avoid site admins using this...
|
||||
|
||||
// Validate and delete the key.
|
||||
$key = validate_user_key($params['qrloginkey'], 'tool_mobile', null);
|
||||
delete_user_key('tool_mobile', $params['userid']);
|
||||
$key = validate_user_key($params['qrloginkey'], 'tool_mobile/qrlogin', null);
|
||||
delete_user_key('tool_mobile/qrlogin', $params['userid']);
|
||||
|
||||
// Double check key belong to user.
|
||||
if ($key->userid != $params['userid']) {
|
||||
|
||||
@@ -66,7 +66,7 @@ class provider implements
|
||||
FROM {user_private_key} k
|
||||
JOIN {user} u ON k.userid = u.id
|
||||
JOIN {context} ctx ON ctx.instanceid = u.id AND ctx.contextlevel = :contextlevel
|
||||
WHERE k.userid = :userid AND k.script = 'tool_mobile'";
|
||||
WHERE k.userid = :userid AND (k.script = 'tool_mobile' OR k.script = 'tool_mobile/qrlogin')";
|
||||
$params = ['userid' => $userid, 'contextlevel' => CONTEXT_USER];
|
||||
$contextlist = new contextlist();
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
@@ -88,6 +88,7 @@ class provider implements
|
||||
|
||||
// Add users based on userkey.
|
||||
\core_userkey\privacy\provider::get_user_contexts_with_script($userlist, $context, 'tool_mobile');
|
||||
\core_userkey\privacy\provider::get_user_contexts_with_script($userlist, $context, 'tool_mobile/qrlogin');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,6 +109,7 @@ class provider implements
|
||||
}
|
||||
// Export associated userkeys.
|
||||
\core_userkey\privacy\provider::export_userkeys($context, [], 'tool_mobile');
|
||||
\core_userkey\privacy\provider::export_userkeys($context, [], 'tool_mobile/qrlogin');
|
||||
}
|
||||
/**
|
||||
* Export all user preferences for the plugin.
|
||||
@@ -138,6 +140,7 @@ class provider implements
|
||||
$userid = $context->instanceid;
|
||||
// Delete all the userkeys.
|
||||
\core_userkey\privacy\provider::delete_userkeys('tool_mobile', $userid);
|
||||
\core_userkey\privacy\provider::delete_userkeys('tool_mobile/qrlogin', $userid);
|
||||
}
|
||||
/**
|
||||
* Delete all user data for the specified user, in the specified contexts.
|
||||
@@ -158,6 +161,7 @@ class provider implements
|
||||
$userid = $context->instanceid;
|
||||
// Delete all the userkeys.
|
||||
\core_userkey\privacy\provider::delete_userkeys('tool_mobile', $userid);
|
||||
\core_userkey\privacy\provider::delete_userkeys('tool_mobile/qrlogin', $userid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -178,5 +182,6 @@ class provider implements
|
||||
|
||||
// Delete all the userkeys.
|
||||
\core_userkey\privacy\provider::delete_userkeys('tool_mobile', $userid);
|
||||
\core_userkey\privacy\provider::delete_userkeys('tool_mobile/qrlogin', $userid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* tool_mobile plugin event handler definition.
|
||||
*
|
||||
* @package tool_mobile
|
||||
* @category event
|
||||
* @copyright 2024 Juan Leyva
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$observers = [
|
||||
[
|
||||
'eventname' => '\core\event\user_loggedin',
|
||||
'callback' => '\tool_mobile\event_handler::handle_user_loggedin',
|
||||
],
|
||||
];
|
||||
@@ -69,6 +69,14 @@ if (empty($service)) {
|
||||
throw new moodle_exception('servicenotavailable', 'webservice');
|
||||
}
|
||||
|
||||
// If the user is using the inapp (embedded) browser, we need to set the Secure and Partitioned attributes to the session cookie.
|
||||
if (\core_useragent::is_moodle_app()) {
|
||||
\core\session\utility\cookie_helper::add_attributes_to_cookie_response_header(
|
||||
"MoodleSession{$CFG->sessioncookie}",
|
||||
['Secure', 'Partitioned'],
|
||||
);
|
||||
}
|
||||
|
||||
require_login(0, false);
|
||||
|
||||
// Require an active user: not guest, not suspended.
|
||||
|
||||
@@ -265,3 +265,16 @@ function tool_mobile_pre_processor_message_send($procname, $data) {
|
||||
$data->fullmessagehtml .= html_writer::tag('p', get_string('readingthisemailgettheapp', 'tool_mobile', $url->out()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to add headers before the HTTP headers are sent.
|
||||
*
|
||||
*/
|
||||
function tool_mobile_before_http_headers() {
|
||||
global $CFG;
|
||||
|
||||
// Set Partitioned and Secure attributes to the MoodleSession cookie if the user is using the Moodle app.
|
||||
if (\core_useragent::is_moodle_app()) {
|
||||
\core\session\utility\cookie_helper::add_attributes_to_cookie_response_header('MoodleSession'.$CFG->sessioncookie, ['Secure', 'Partitioned']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,8 @@ class provider_test extends \core_privacy\tests\provider_testcase {
|
||||
$context1 = \context_user::instance($user1->id);
|
||||
$context2 = \context_user::instance($user2->id);
|
||||
$key1 = get_user_key('tool_mobile', $user1->id);
|
||||
$key2 = get_user_key('tool_mobile', $user2->id);
|
||||
$key2 = get_user_key('tool_mobile/qrlogin', $user1->id);
|
||||
$key3 = get_user_key('tool_mobile', $user2->id);
|
||||
|
||||
// Ensure only user1 is found in context1.
|
||||
$userlist = new \core_privacy\local\request\userlist($context1, $component);
|
||||
@@ -174,12 +175,15 @@ class provider_test extends \core_privacy\tests\provider_testcase {
|
||||
$context1 = \context_user::instance($user1->id);
|
||||
$context2 = \context_user::instance($user2->id);
|
||||
$keyvalue1 = get_user_key('tool_mobile', $user1->id);
|
||||
$keyvalue2 = get_user_key('tool_mobile', $user2->id);
|
||||
$keyvalue2 = get_user_key('tool_mobile/qrlogin', $user1->id);
|
||||
$keyvalue3 = get_user_key('tool_mobile', $user2->id);
|
||||
$key1 = $DB->get_record('user_private_key', ['value' => $keyvalue1]);
|
||||
|
||||
// Before deletion, we should have 2 user_private_keys.
|
||||
// Before deletion, we should have 2 user_private_keys for tool_mobile and one for tool_mobile/qrlogin.
|
||||
$count = $DB->count_records('user_private_key', ['script' => 'tool_mobile']);
|
||||
$this->assertEquals(2, $count);
|
||||
$count = $DB->count_records('user_private_key', ['script' => 'tool_mobile/qrlogin']);
|
||||
$this->assertEquals(1, $count);
|
||||
|
||||
// Ensure deleting wrong user in the user context does nothing.
|
||||
$approveduserids = [$user2->id];
|
||||
@@ -197,6 +201,8 @@ class provider_test extends \core_privacy\tests\provider_testcase {
|
||||
// Ensure only user1's data is deleted, user2's remains.
|
||||
$count = $DB->count_records('user_private_key', ['script' => 'tool_mobile']);
|
||||
$this->assertEquals(1, $count);
|
||||
$count = $DB->count_records('user_private_key', ['script' => 'tool_mobile/qrlogin']);
|
||||
$this->assertEquals(0, $count);
|
||||
|
||||
$params = ['script' => $component];
|
||||
$userid = $DB->get_field_select('user_private_key', 'userid', 'script = :script', $params);
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
$plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->version = 2022112801; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->requires = 2022111800; // Requires this Moodle version.
|
||||
$plugin->component = 'tool_mobile'; // Full name of the plugin (used for diagnostics).
|
||||
$plugin->dependencies = array(
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -121,7 +121,6 @@ define(['tool_moodlenet/validator',
|
||||
});
|
||||
// Trigger the transition between 'pages'.
|
||||
carousel.carousel(2);
|
||||
// eslint-disable-next-line max-len
|
||||
modal.setFooter(Templates.render('tool_moodlenet/chooser_footer_close_mnet', {}));
|
||||
};
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ if ($cancel) {
|
||||
}
|
||||
redirect($url);
|
||||
} else if ($continue) {
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
// Handle backups.
|
||||
if (strtolower($importinfo->get_resource()->get_extension()) == 'mbz') {
|
||||
|
||||
@@ -81,7 +81,7 @@ switch ($config->type) {
|
||||
}
|
||||
|
||||
if ($import && $module) {
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
$handlerinfo = $handlerregistry->get_resource_handler_for_mod_and_strategy($importinfo->get_resource(), $module, $strategy);
|
||||
if (is_null($handlerinfo)) {
|
||||
|
||||
+1
-1
@@ -116,7 +116,7 @@ class auth_plugin_lti extends \auth_plugin_base {
|
||||
if (isloggedin()) {
|
||||
// If a different user is currently logged in, authenticate the linked user instead.
|
||||
global $USER;
|
||||
if ((int) $USER->id !== $user->id) {
|
||||
if ($USER->id !== $user->id) {
|
||||
complete_user_login($user);
|
||||
}
|
||||
// If the linked user is already logged in, skip the call to complete_user_login() because this affects deep linking
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace auth_lti\local\ltiadvantage\event;
|
||||
|
||||
use auth_lti\local\ltiadvantage\utility\cookie_helper;
|
||||
use core\event\user_loggedin;
|
||||
|
||||
/**
|
||||
* Event handler for auth_lti.
|
||||
*
|
||||
* @package auth_lti
|
||||
* @copyright 2024 Jake Dallimore <jrhdallimore@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class event_handler {
|
||||
|
||||
/**
|
||||
* Allows the plugin to augment Set-Cookie headers when the user_loggedin event is fired as part of complete_user_login() calls.
|
||||
*
|
||||
* @param user_loggedin $event the event
|
||||
* @return void
|
||||
*/
|
||||
public static function handle_user_loggedin(user_loggedin $event): void {
|
||||
// The event data isn't important here. The intent of this listener is to ensure that the MoodleSession cookie is set up
|
||||
// properly during LTI launches + login. This means two things:
|
||||
// i) it's set with SameSite=None; Secure; where possible (since OIDC needs HTTPS this will almost always be possible).
|
||||
// ii) it set with the 'Partitioned' attribute, when required.
|
||||
// The former ensures cross-site cookies are sent for embedded launches. The latter is an opt-in flag needed to use Chrome's
|
||||
// partitioning mechanism, CHIPS.
|
||||
if (cookie_helper::cookies_supported()) {
|
||||
cookie_helper::setup_session_cookie();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace auth_lti\local\ltiadvantage\utility;
|
||||
use core\session\utility\cookie_helper as core_cookie_helper;
|
||||
|
||||
/**
|
||||
* Helper class providing utils dealing with cookies in LTI, particularly 3rd party cookies.
|
||||
*
|
||||
* @package auth_lti
|
||||
* @copyright 2024 Jake Dallimore <jrhdallimore@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
final class cookie_helper {
|
||||
|
||||
/** @var int Cookies are not supported. */
|
||||
public const COOKIE_METHOD_NOT_SUPPORTED = 0;
|
||||
|
||||
/** @var int Cookies are supported without explicit partitioning. */
|
||||
public const COOKIE_METHOD_NO_PARTITIONING = 1;
|
||||
|
||||
/** @var int Cookies are supported via explicit partitioning. */
|
||||
public const COOKIE_METHOD_EXPLICIT_PARTITIONING = 2;
|
||||
|
||||
/**
|
||||
* Check whether cookies can be used with the current user agent and, if so, via what method they are set.
|
||||
*
|
||||
* Currently, this tries 2 modes of setting a test cookie:
|
||||
* 1. Setting a SameSite=None, Secure cookie. This will work in any first party context, and in 3rd party contexts for
|
||||
* any browsers supporting automatic partitioning of 3rd party cookies (E.g. Firefox, Brave).
|
||||
* 2. If 1 fails, setting a cookie with the Chrome 'Partitioned' attribute included, opting that cookie into CHIPS. This will
|
||||
* work for Chrome.
|
||||
*
|
||||
* Upon completion of the cookie check, the check sets a SESSION flag indicating the method used to set the cookie, and upgrades
|
||||
* the session cookie ('MoodleSession') using the respective method. This ensure the session cookie will continue to be sent.
|
||||
*
|
||||
* Then, the following methods can be used by client code to query whether the UA supports cookies, and how:
|
||||
* @see self::cookies_supported() - whether it could be set at all.
|
||||
* @see self::get_cookies_supported_mode() - if a cookie could be set, what mode was used to set it.
|
||||
*
|
||||
* This permits client code to make sure it's setting its cookies appropriately (via the advertised method), and allows it to
|
||||
* present notices - such as in the case where a given UA is found to be lacking the requisite cookie support.
|
||||
* E.g.
|
||||
* cookie_helper::do_cookie_check($mypageurl);
|
||||
* if (!cookie_helper::cookies_supported()) {
|
||||
* // Print a notice stating that cookie support is required.
|
||||
* }
|
||||
* // Elsewhere in other client code...
|
||||
* if (cookie_helper::get_cookies_supported_mode() === cookie_helper::COOKIE_METHOD_EXPLICIT_PARTITIONING) {
|
||||
* // Set a cookie, making sure to use the helper to also opt-in to partitioning.
|
||||
* setcookie('myauthcookie', 'myauthcookievalue', ['samesite' => 'None', 'secure' => true]);
|
||||
* cookie_helper::add_partitioning_to_cookie('myauthcookie');
|
||||
* }
|
||||
*
|
||||
* @param \moodle_url $pageurl the URL of the page making the check, used to redirect back to after setting test cookies.
|
||||
* @return void
|
||||
*/
|
||||
public static function do_cookie_check(\moodle_url $pageurl): void {
|
||||
global $_COOKIE, $SESSION, $CFG;
|
||||
$cookiecheck1 = optional_param('cookiecheck1', null, PARAM_INT);
|
||||
$cookiecheck2 = optional_param('cookiecheck2', null, PARAM_INT);
|
||||
|
||||
if (empty($cookiecheck1)) {
|
||||
// Start the cookie check. Set two test cookies - one samesite none, and one partitioned - and redirect.
|
||||
// Set cookiecheck to show the check has started.
|
||||
self::set_test_cookie('cookiecheck1', self::COOKIE_METHOD_NO_PARTITIONING);
|
||||
self::set_test_cookie('cookiecheck2', self::COOKIE_METHOD_EXPLICIT_PARTITIONING, true);
|
||||
$pageurl->params([
|
||||
'cookiecheck1' => self::COOKIE_METHOD_NO_PARTITIONING,
|
||||
'cookiecheck2' => self::COOKIE_METHOD_EXPLICIT_PARTITIONING,
|
||||
]);
|
||||
|
||||
// LTI needs to guarantee the 'SameSite=None', 'Secure' (and sometimes 'Partitioned') attributes are set on the
|
||||
// MoodleSession cookie. This is done via manipulation of the outgoing headers after the cookie check redirect. To
|
||||
// guarantee these outgoing Set-Cookie headers will be created after the redirect, expire the current cookie.
|
||||
core_cookie_helper::expire_moodlesession();
|
||||
|
||||
redirect($pageurl);
|
||||
} else {
|
||||
// Have already started a cookie check, so check the result.
|
||||
$cookie1received = isset($_COOKIE['cookiecheck1']) && $_COOKIE['cookiecheck1'] == $cookiecheck1;
|
||||
$cookie2received = isset($_COOKIE['cookiecheck2']) && $_COOKIE['cookiecheck2'] == $cookiecheck2;
|
||||
|
||||
if ($cookie1received || $cookie2received) {
|
||||
// The test cookie could be set and received.
|
||||
// Set a session flag storing the method used to set it, and make sure the session cookie uses this method.
|
||||
$cookiemethod = $cookie1received ? self::COOKIE_METHOD_NO_PARTITIONING : self::COOKIE_METHOD_EXPLICIT_PARTITIONING;
|
||||
$SESSION->auth_lti_cookie_method = $cookiemethod;
|
||||
|
||||
self::setup_session_cookie();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If a cookie check has been made, returns whether cookies could be set or not.
|
||||
*
|
||||
* @return bool whether cookies are supported or not.
|
||||
*/
|
||||
public static function cookies_supported(): bool {
|
||||
return self::get_cookies_supported_method() !== self::COOKIE_METHOD_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* If a cookie check has been made, gets the method used to set a cookie, or self::COOKIE_METHOD_NOT_SUPPORTED if not supported.
|
||||
*
|
||||
* For cookie methods:
|
||||
* @see self::COOKIE_METHOD_NOT_SUPPORTED
|
||||
* @see self::COOKIE_METHOD_NO_PARTITIONING
|
||||
* @see self::COOKIE_METHOD_EXPLICIT_PARTITIONING
|
||||
*
|
||||
* @return int the constant representing the method by which the cookie was set, or not.
|
||||
*/
|
||||
public static function get_cookies_supported_method(): int {
|
||||
global $SESSION;
|
||||
return $SESSION->auth_lti_cookie_method ?? self::COOKIE_METHOD_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the session cookie according to the method used in the cookie check, and with SameSite=None; Secure attributes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function setup_session_cookie(): void {
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/sessionlib.php');
|
||||
|
||||
if (is_moodle_cookie_secure()) {
|
||||
$atts = ['SameSite=None', 'Secure'];
|
||||
if (self::get_cookies_supported_method() == self::COOKIE_METHOD_EXPLICIT_PARTITIONING) {
|
||||
$atts[] = 'Partitioned';
|
||||
}
|
||||
core_cookie_helper::add_attributes_to_cookie_response_header('MoodleSession' . $CFG->sessioncookie, $atts);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a test cookie, using SameSite=None; Secure; attributes if possible, and with or without partitioning opt-in.
|
||||
*
|
||||
* @param string $name cookie name
|
||||
* @param string $value cookie value
|
||||
* @param bool $partitioned whether to try to add partitioning opt-in, which requires secure cookies (https sites).
|
||||
* @return void
|
||||
*/
|
||||
private static function set_test_cookie(string $name, string $value, bool $partitioned = false): void {
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/sessionlib.php');
|
||||
|
||||
$atts = ['expires' => time() + 30];
|
||||
if (is_moodle_cookie_secure()) {
|
||||
$atts['samesite'] = 'none';
|
||||
$atts['secure'] = true;
|
||||
}
|
||||
setcookie($name, $value, $atts);
|
||||
|
||||
if (is_moodle_cookie_secure() && $partitioned) {
|
||||
core_cookie_helper::add_attributes_to_cookie_response_header($name, ['Partitioned']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,33 +33,32 @@ class renderer extends \plugin_renderer_base {
|
||||
* @return string the html.
|
||||
*/
|
||||
public function render_account_binding_options_page(int $provisioningmode): string {
|
||||
|
||||
$formaction = new \moodle_url('/auth/lti/login.php');
|
||||
$notification = new notification(get_string('firstlaunchnotice', 'auth_lti'), \core\notification::INFO, false);
|
||||
$noauthnotice = new notification(get_string('firstlaunchnoauthnotice', 'auth_lti', get_docs_url('Publish_as_LTI_tool')),
|
||||
\core\notification::WARNING, false);
|
||||
$cancreateaccounts = !get_config('moodle', 'authpreventaccountcreation');
|
||||
if ($provisioningmode == \auth_plugin_lti::PROVISIONING_MODE_PROMPT_EXISTING_ONLY) {
|
||||
$cancreateaccounts = false;
|
||||
}
|
||||
|
||||
$accountinfo = ['isloggedin' => isloggedin()];
|
||||
$accountinfo = [];
|
||||
if (isloggedin()) {
|
||||
global $USER;
|
||||
$accountinfo = array_merge($accountinfo, [
|
||||
$accountinfo = [
|
||||
'firstname' => $USER->firstname,
|
||||
'lastname' => $USER->lastname,
|
||||
'email' => $USER->email,
|
||||
'picturehtml' => $this->output->user_picture($USER, ['size' => 35, 'class' => 'round']),
|
||||
]);
|
||||
];
|
||||
}
|
||||
|
||||
$context = [
|
||||
'isloggedin' => isloggedin(),
|
||||
'info' => $notification->export_for_template($this),
|
||||
'formaction' => $formaction->out(),
|
||||
'sesskey' => sesskey(),
|
||||
'accountinfo' => $accountinfo,
|
||||
'cancreateaccounts' => $cancreateaccounts,
|
||||
'noauthnotice' => $noauthnotice->export_for_template($this)
|
||||
];
|
||||
return parent::render_from_template('auth_lti/local/ltiadvantage/login', $context);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* LTI Auth plugin event handler definition.
|
||||
*
|
||||
* @package auth_lti
|
||||
* @category event
|
||||
* @copyright 2024 Jake Dallimore <jrhdallimore@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$observers = [
|
||||
[
|
||||
'eventname' => '\core\event\user_loggedin',
|
||||
'callback' => '\auth_lti\local\ltiadvantage\event\event_handler::handle_user_loggedin',
|
||||
],
|
||||
];
|
||||
@@ -35,7 +35,7 @@ $string['firstlaunchnoauthnotice'] = 'To link your existing account you must be
|
||||
$string['getstartedwithnewaccount'] = 'Get started with a new account';
|
||||
$string['haveexistingaccount'] = 'I have an existing account';
|
||||
$string['linkthisaccount'] = 'Link this account';
|
||||
$string['mustbeloggedin'] = 'You need to be logged in to your existing account';
|
||||
$string['mustbeloggedin'] = 'Sign in to link your existing account';
|
||||
$string['pluginname'] = 'LTI';
|
||||
$string['privacy:metadata:auth_lti'] = 'LTI authentication';
|
||||
$string['privacy:metadata:auth_lti:authsubsystem'] = 'This plugin is connected to the authentication subsystem.';
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
* info - a notification describing the first launch options
|
||||
* cancreateaccounts - whether or not the user is allowed to create auth_lti accounts
|
||||
* accountinfo - information about the user, importantly whether they are logged in or not.
|
||||
* noauthnotice - a notification telling the user they must be authenticated to link accounts. Only relevant when not logged in.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
@@ -46,19 +45,12 @@
|
||||
"issuccess": true
|
||||
},
|
||||
"cancreateaccounts": true,
|
||||
"isloggedin": true,
|
||||
"accountinfo": {
|
||||
"isloggedin": true,
|
||||
"firstname": "John",
|
||||
"lastname": "Smith",
|
||||
"email": "john@example.com",
|
||||
"picturehtml": "<img src=\"http://site.example.com/pluginfile.php/5/user/icon/boost/f2?rev=99\" class=\"round\" alt=\"\" width=\"35\" height=\"35\">"
|
||||
},
|
||||
"noauthnotice": {
|
||||
"message": "To link your existing account you must be logged in to the site...",
|
||||
"extraclasses": "",
|
||||
"announce": false,
|
||||
"closebutton": false,
|
||||
"iswarning": true
|
||||
}
|
||||
}
|
||||
}}
|
||||
@@ -79,8 +71,8 @@
|
||||
<div class="card-body text-center d-flex flex-column">
|
||||
<i class="fa fa-user-circle-o fa-2x link"></i>
|
||||
<h4 class="card-title">{{#str}} useexistingaccount, auth_lti {{/str}}</h4>
|
||||
{{#accountinfo}}
|
||||
{{#isloggedin}}
|
||||
{{#accountinfo}}
|
||||
<p class="card-text mt-2">
|
||||
<span class="text-muted">
|
||||
{{#str}} currentlyloggedinas, auth_lti {{/str}}
|
||||
@@ -90,14 +82,12 @@
|
||||
{{firstname}} {{lastname}} ({{email}})
|
||||
</p>
|
||||
<input type="submit" class="btn btn-primary mt-auto" name="existing_account" value="{{#str}} linkthisaccount, auth_lti {{/str}}">
|
||||
{{/accountinfo}}
|
||||
{{/isloggedin}}
|
||||
{{^isloggedin}}
|
||||
<p class="card-text text-muted">{{#str}} mustbeloggedin, auth_lti {{/str}}</p>
|
||||
{{#noauthnotice}}
|
||||
{{> core/notification}}
|
||||
{{/noauthnotice}}
|
||||
<input type="submit" class="btn btn-primary mt-auto" name="existing_account" value="{{#str}} login, moodle {{/str}}">
|
||||
{{/isloggedin}}
|
||||
{{/accountinfo}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,6 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->version = 2022112801; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->requires = 2022111800; // Requires this Moodle version.
|
||||
$plugin->component = 'auth_lti'; // Full name of the plugin (used for diagnostics).
|
||||
|
||||
@@ -119,3 +119,97 @@ Feature: Award badges with separate groups
|
||||
And I press "Award badge"
|
||||
# Teacher 2 shouldn't be able to go further
|
||||
Then I should see "Sorry, but you need to be part of a group to see this page."
|
||||
|
||||
@javascript
|
||||
Scenario: Editing teacher can award badge to members of separate groups
|
||||
Given I am on the "Course 1" course page logged in as teacher1
|
||||
And I navigate to "Badges > Manage badges" in current page administration
|
||||
And I follow "Course Badge"
|
||||
And I press "Award badge"
|
||||
When I set the field "role" to "Teacher"
|
||||
# Confirm that editing teacher sees a separate groups dropdown menu.
|
||||
Then "Separate groups" "select" should exist
|
||||
And I should see "All participants" in the "Separate groups" "select"
|
||||
# Confirm that all participants are displayed when All participants is selected.
|
||||
And I should see "Student 1" in the "potentialrecipients[]" "select"
|
||||
And I should see "Student 2" in the "potentialrecipients[]" "select"
|
||||
And I should see "Teacher 1" in the "potentialrecipients[]" "select"
|
||||
And I should see "Teacher 2" in the "potentialrecipients[]" "select"
|
||||
And I set the field "Separate groups" to "Class A"
|
||||
# Confirm that only members of selected group are displayed
|
||||
And I should not see "Student 1" in the "potentialrecipients[]" "select"
|
||||
And I should not see "Teacher 1" in the "potentialrecipients[]" "select"
|
||||
And I should see "Student 2" in the "potentialrecipients[]" "select"
|
||||
And I should see "Teacher 2" in the "potentialrecipients[]" "select"
|
||||
And I set the field "Separate groups" to "Class B"
|
||||
And I should not see "Student 2" in the "potentialrecipients[]" "select"
|
||||
And I should not see "Teacher 2" in the "potentialrecipients[]" "select"
|
||||
And I should see "Student 1" in the "potentialrecipients[]" "select"
|
||||
And I should see "Teacher 1" in the "potentialrecipients[]" "select"
|
||||
|
||||
@javascript
|
||||
Scenario Outline: Teacher can award badge to members of visible groups
|
||||
Given I am on the "Course 1" "course editing" page logged in as teacher1
|
||||
And I expand all fieldsets
|
||||
# Set the group mode to visible groups.
|
||||
And I set the field "Group mode" to "Visible groups"
|
||||
And I press "Save and display"
|
||||
When I am on the "Course 1" course page logged in as <loggedinuser>
|
||||
And I navigate to "Badges > Manage badges" in current page administration
|
||||
And I follow "Course Badge"
|
||||
And I press "Award badge"
|
||||
And I set the field "role" to "<awarder>"
|
||||
# Confirm that teachers see a visible groups dropdown menu.
|
||||
Then "Visible groups" "select" should exist
|
||||
# Confirm that My groups option group exists.
|
||||
And "optgroup[label='My groups']" "css_element" should exist in the "select[name='group']" "css_element"
|
||||
# Confirm that Other groups option group exists.
|
||||
And "optgroup[label='Other groups']" "css_element" should exist in the "select[name='group']" "css_element"
|
||||
# Confirm that all participants are displayed when All participants is selected.
|
||||
And I set the field "Visible groups" to "All participants"
|
||||
And I should see "Student 1" in the "potentialrecipients[]" "select"
|
||||
And I should see "Student 2" in the "potentialrecipients[]" "select"
|
||||
And I should see "Teacher 1" in the "potentialrecipients[]" "select"
|
||||
And I should see "Teacher 2" in the "potentialrecipients[]" "select"
|
||||
# Confirm that only members of selected group are displayed.
|
||||
And I set the field "Visible groups" to "Class A"
|
||||
And I should not see "Student 1" in the "potentialrecipients[]" "select"
|
||||
And I should not see "Teacher 1" in the "potentialrecipients[]" "select"
|
||||
And I should see "Student 2" in the "potentialrecipients[]" "select"
|
||||
And I should see "Teacher 2" in the "potentialrecipients[]" "select"
|
||||
And I set the field "Visible groups" to "Class B"
|
||||
And I should not see "Student 2" in the "potentialrecipients[]" "select"
|
||||
And I should not see "Teacher 2" in the "potentialrecipients[]" "select"
|
||||
And I should see "Student 1" in the "potentialrecipients[]" "select"
|
||||
And I should see "Teacher 1" in the "potentialrecipients[]" "select"
|
||||
|
||||
Examples:
|
||||
| loggedinuser | awarder |
|
||||
| teacher1 | Teacher |
|
||||
| teacher2 | Non-editing teacher |
|
||||
|
||||
@javascript
|
||||
Scenario Outline: Teacher can award badge to members when group mode is set to no groups
|
||||
Given I am on the "Course 1" "course editing" page logged in as teacher1
|
||||
And I expand all fieldsets
|
||||
# Set the group mode to no groups.
|
||||
And I set the field "Group mode" to "No groups"
|
||||
And I press "Save and display"
|
||||
When I am on the "Course 1" course page logged in as <loggedinuser>
|
||||
And I navigate to "Badges > Manage badges" in current page administration
|
||||
And I follow "Course Badge"
|
||||
And I press "Award badge"
|
||||
And I set the field "role" to "<awarder>"
|
||||
# Confirm that no group dropdowns don't exist.
|
||||
Then "Separate groups" "select" should not exist
|
||||
And "Visible groups" "select" should not exist
|
||||
# Confirm all participants are displayed.
|
||||
And I should see "Student 1" in the "potentialrecipients[]" "select"
|
||||
And I should see "Student 2" in the "potentialrecipients[]" "select"
|
||||
And I should see "Teacher 1" in the "potentialrecipients[]" "select"
|
||||
And I should see "Teacher 2" in the "potentialrecipients[]" "select"
|
||||
|
||||
Examples:
|
||||
| loggedinuser | awarder |
|
||||
| teacher1 | Teacher |
|
||||
| teacher2 | Non-editing teacher |
|
||||
|
||||
@@ -58,7 +58,7 @@ class helper {
|
||||
FROM {block_recentlyaccesseditems} rai
|
||||
JOIN {course} c ON c.id = rai.courseid
|
||||
WHERE userid = :userid
|
||||
ORDER BY rai.timeaccess DESC";
|
||||
ORDER BY rai.timeaccess DESC, rai.id DESC";
|
||||
$records = $DB->get_records_sql($sql, $paramsql);
|
||||
$order = 0;
|
||||
|
||||
|
||||
@@ -101,13 +101,14 @@ class block_tag_flickr extends block_base {
|
||||
$request .= '&api_key='.FLICKR_DEV_KEY;
|
||||
$request .= '&photoset_id='.$this->config->photoset;
|
||||
$request .= '&per_page='.$numberofphotos;
|
||||
$request .= '&format=php_serial';
|
||||
$request .= '&format=json';
|
||||
// We need to add nojsoncallback=? here, otherwise, Flickr will return the jsonFlickrApi object.
|
||||
$request .= '&nojsoncallback=?';
|
||||
|
||||
$response = $this->fetch_request($request);
|
||||
|
||||
$search = @unserialize($response);
|
||||
if ($search === false && $search != serialize(false)) {
|
||||
// The response didn't appear to be anything serialized, exit...
|
||||
$search = @json_decode($response, true);
|
||||
if (!is_array($search) || json_last_error() !== JSON_ERROR_NONE) {
|
||||
// The response didn't appear to be in correct format.
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -126,13 +127,14 @@ class block_tag_flickr extends block_base {
|
||||
$request .= '&tags='.$tagscsv;
|
||||
$request .= '&per_page='.$numberofphotos;
|
||||
$request .= '&sort='.$sortby;
|
||||
$request .= '&format=php_serial';
|
||||
$request .= '&format=json';
|
||||
// We need to add nojsoncallback=? here, otherwise, Flickr will return the jsonFlickrApi object.
|
||||
$request .= '&nojsoncallback=?';
|
||||
|
||||
$response = $this->fetch_request($request);
|
||||
|
||||
$search = @unserialize($response);
|
||||
if ($search === false && $search != serialize(false)) {
|
||||
// The response didn't appear to be anything serialized, exit...
|
||||
$search = @json_decode($response, true);
|
||||
if (!is_array($search) || json_last_error() !== JSON_ERROR_NONE) {
|
||||
// The response didn't appear to be in correct format.
|
||||
return;
|
||||
}
|
||||
$photos = array_values($search['photos']['photo']);
|
||||
|
||||
Vendored
+2
-1
@@ -608,7 +608,8 @@ class cache implements cache_loader {
|
||||
// store; parent method will have set it to all stores if needed.
|
||||
if ($setaftervalidation) {
|
||||
$lock = null;
|
||||
if (!empty($this->requirelockingbeforewrite)) {
|
||||
// Only try to acquire a lock for this cache if we do not already have one.
|
||||
if (!empty($this->requirelockingbeforewrite) && !$this->check_lock_state($key)) {
|
||||
$lock = $this->acquire_lock($key);
|
||||
}
|
||||
if ($requiredversion === self::VERSION_NONE) {
|
||||
|
||||
@@ -78,7 +78,8 @@ trait eventtype {
|
||||
if (!empty($eventtypes['user']) && count($options) == 1) {
|
||||
$mform->addElement('hidden', 'eventtype');
|
||||
$mform->setType('eventtype', PARAM_TEXT);
|
||||
$mform->setDefault('eventtype', 'user');
|
||||
$mform->hardFreeze('eventtype');
|
||||
$mform->setConstant('eventtype', 'user');
|
||||
return;
|
||||
} else {
|
||||
$mform->addElement('select', 'eventtype', get_string('eventkind', 'calendar'), $options);
|
||||
|
||||
@@ -106,7 +106,7 @@ class managesubscriptions extends \moodleform {
|
||||
$courseid = (!empty($data[$coursekey])) ? $data[$coursekey] : null;
|
||||
$eventtypes = calendar_get_allowed_event_types($courseid);
|
||||
|
||||
if (empty($eventtype) || !isset($eventtypes[$eventtype])) {
|
||||
if (empty($eventtype) || !isset($eventtypes[$eventtype]) || $eventtypes[$eventtype] == false) {
|
||||
$errors['eventtype'] = get_string('invalideventtype', 'calendar');
|
||||
}
|
||||
|
||||
|
||||
@@ -898,7 +898,17 @@ class core_calendar_external extends external_api {
|
||||
$courseid = (!empty($data[$coursekey])) ? $data[$coursekey] : null;
|
||||
$editoroptions = \core_calendar\local\event\forms\create::build_editor_options($context);
|
||||
$formoptions = ['editoroptions' => $editoroptions, 'courseid' => $courseid];
|
||||
$formoptions['eventtypes'] = calendar_get_allowed_event_types($courseid);
|
||||
$allowedeeventtypes = calendar_get_allowed_event_types($courseid);
|
||||
|
||||
// Event type validation.
|
||||
if (in_array(true, $allowedeeventtypes, true) === false) {
|
||||
throw new \moodle_exception('nopermissiontoupdatecalendar');
|
||||
}
|
||||
if (empty($eventtype) || !isset($allowedeeventtypes[$eventtype]) || $allowedeeventtypes[$eventtype] == false) {
|
||||
return ['validationerror' => true];
|
||||
}
|
||||
|
||||
$formoptions['eventtypes'] = $allowedeeventtypes;
|
||||
if ($courseid) {
|
||||
require_once($CFG->libdir . '/grouplib.php');
|
||||
$groupcoursedata = groups_get_course_data($courseid);
|
||||
|
||||
@@ -3555,6 +3555,18 @@ function calendar_get_view(\calendar_information $calendar, $view, $includenavig
|
||||
}
|
||||
}
|
||||
|
||||
// Check if $data has events.
|
||||
if (isset($data->events)) {
|
||||
// Let's check and sanitize all "name" in $data->events before it's sent to front end.
|
||||
foreach ($data->events as $d) {
|
||||
$name = $d->name ?? null;
|
||||
// Encode special characters if our decoded name does not match the original name.
|
||||
if ($name && (html_entity_decode($name) !== $name)) {
|
||||
$d->name = htmlspecialchars(html_entity_decode($name), ENT_QUOTES, 'utf-8');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [$data, $template];
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ Feature: Allow to mark course as completed without cron for activity completion
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Completion course | CC1 | 0 |
|
||||
| fullname | shortname | category | enablecompletion |
|
||||
| Completion course | CC1 | 0 | 1 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| student1 | Student | First | student1@example.com |
|
||||
@@ -25,11 +25,7 @@ Feature: Allow to mark course as completed without cron for activity completion
|
||||
And the following "blocks" exist:
|
||||
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
|
||||
| completionstatus | Course | CC1 | course-view-* | side-pre |
|
||||
And I am on the "Completion course" "course editing" page logged in as admin
|
||||
And I expand all fieldsets
|
||||
And I set the field "Enable completion tracking" to "Yes"
|
||||
And I click on "Save and display" "button"
|
||||
And I am on the "Test assignment name" "assign activity editing" page
|
||||
And I am on the "Test assignment name" "assign activity editing" page logged in as admin
|
||||
And I follow "Expand all"
|
||||
And I set the field "Completion tracking" to "Show activity as complete when conditions are met"
|
||||
And I set the field "completionusegrade" to "1"
|
||||
|
||||
@@ -6,8 +6,8 @@ Feature: Show activity completion status or activity completion configuration on
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
| fullname | shortname | category | enablecompletion |
|
||||
| Course 1 | C1 | 0 | 1 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | First | teacher1@example.com |
|
||||
@@ -18,12 +18,6 @@ Feature: Show activity completion status or activity completion configuration on
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| teacher2 | C1 | teacher |
|
||||
| student1 | C1 | student |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I navigate to "Settings" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Enable completion tracking | Yes |
|
||||
And I press "Save and display"
|
||||
And the following "activities" exist:
|
||||
| activity | course | idnumber | name | intro | completion | completionview | completionexpected |
|
||||
| assign | C1 | assign1 | Test assignment name | Test assignment description | 2 | 1 | 0 |
|
||||
|
||||
@@ -7,8 +7,8 @@ Feature: Allow students to manually mark an activity as complete
|
||||
@javascript
|
||||
Scenario: Mark an activity as completed
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
| fullname | shortname | category | enablecompletion |
|
||||
| Course 1 | C1 | 0 | 1 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | First | teacher1@example.com |
|
||||
@@ -18,18 +18,11 @@ Feature: Allow students to manually mark an activity as complete
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
And the following "activity" exists:
|
||||
| activity | forum |
|
||||
| course | C1 |
|
||||
| name | Test forum name |
|
||||
| activity | forum |
|
||||
| course | C1 |
|
||||
| name | Test forum name |
|
||||
| completion | 1 |
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
And I navigate to "Settings" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Enable completion tracking | Yes |
|
||||
And I press "Save and display"
|
||||
And I am on the "Test forum name" "forum activity editing" page
|
||||
And I set the following fields to these values:
|
||||
| completion | 1 |
|
||||
And I press "Save and return to course"
|
||||
And "Student First" user has not completed "Test forum name" activity
|
||||
And I am on the "Course 1" course page logged in as student1
|
||||
When I toggle the manual completion state of "Test forum name"
|
||||
|
||||
@@ -6,8 +6,8 @@ Feature: Restrict sections availability through completion or grade conditions
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
| fullname | shortname | category | enablecompletion |
|
||||
| Course 1 | C1 | 0 | 1 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | First | teacher1@example.com |
|
||||
@@ -23,15 +23,11 @@ Feature: Restrict sections availability through completion or grade conditions
|
||||
|
||||
@javascript
|
||||
Scenario: Show section greyed-out to student when completion condition is not satisfied
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I navigate to "Settings" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Enable completion tracking | Yes |
|
||||
And I press "Save and display"
|
||||
And the following "activities" exist:
|
||||
Given the following "activities" exist:
|
||||
| activity | course | section | intro | completion | idnumber |
|
||||
| label | C1 | 1 | Test label | 1 | 1 |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
When I edit the section "2"
|
||||
And I expand all fieldsets
|
||||
And I click on "Add restriction..." "button"
|
||||
|
||||
@@ -46,7 +46,7 @@ $filename = str_replace('/', '', str_replace(' ', '_', $courseinfo->shortname))
|
||||
|
||||
// If download confirmed, prepare and start the zipstream of the course download content.
|
||||
if ($isdownload) {
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
|
||||
$exportoptions = null;
|
||||
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
@@ -8,6 +8,6 @@ define("core_courseformat/local/courseindex/cm",["exports","core_courseformat/lo
|
||||
* @class core_courseformat/local/courseindex/cm
|
||||
* @copyright 2021 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_dndcmitem=_interopRequireDefault(_dndcmitem),_templates=_interopRequireDefault(_templates),_prefetch=_interopRequireDefault(_prefetch),_config=_interopRequireDefault(_config);_prefetch.default.prefetchTemplate("core_courseformat/local/courseindex/cmcompletion");class Component extends _dndcmitem.default{create(){this.name="courseindex_cm",this.selectors={CM_NAME:"[data-for='cm_name']",CM_COMPLETION:"[data-for='cm_completion']"},this.classes={CMHIDDEN:"dimmed",LOCKED:"editinprogress",RESTRICTIONS:"restrictions",PAGEITEM:"pageitem",INDENTED:"indented"},this.id=this.element.dataset.id}static init(target,selectors){return new Component({element:document.getElementById(target),selectors:selectors})}stateReady(state){this.configDragDrop(this.id);const cm=state.cm.get(this.id),course=state.course;this._refreshCompletion({state:state,element:cm});const anchor=new URL(window.location.href).hash.replace("#","");(window.location.href==cm.url||window.location.href.includes(course.baseurl)&&anchor==cm.anchor)&&(this.reactive.dispatch("setPageItem","cm",this.id),this.element.scrollIntoView({block:"center"})),_config.default.contextid!=_config.default.courseContextId&&_config.default.contextInstanceId==this.id&&(this.reactive.dispatch("setPageItem","cm",this.id,!0),this.element.scrollIntoView({block:"center"})),cm.uservisible||this.addEventListener(this.getElement(this.selectors.CM_NAME),"click",this._activityAnchor)}getWatchers(){return[{watch:"cm[".concat(this.id,"]:deleted"),handler:this.remove},{watch:"cm[".concat(this.id,"]:updated"),handler:this._refreshCm},{watch:"cm[".concat(this.id,"].completionstate:updated"),handler:this._refreshCompletion},{watch:"course.pageItem:updated",handler:this._refreshPageItem}]}_refreshCm(_ref){var _element$dragging,_element$locked,_element$hascmrestric;let{element:element}=_ref;this.element.classList.toggle(this.classes.CMHIDDEN,!element.visible),this.getElement(this.selectors.CM_NAME).innerHTML=element.name,this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.element.classList.toggle(this.classes.RESTRICTIONS,null!==(_element$hascmrestric=element.hascmrestrictions)&&void 0!==_element$hascmrestric&&_element$hascmrestric),this.element.classList.toggle(this.classes.INDENTED,element.indent),this.locked=element.locked}_refreshPageItem(_ref2){let{element:element}=_ref2;if(!element.pageItem)return;const isPageId="cm"==element.pageItem.type&&element.pageItem.id==this.id;this.element.classList.toggle(this.classes.PAGEITEM,isPageId),isPageId&&!this.reactive.isEditing&&this.element.scrollIntoView({block:"nearest"})}async _refreshCompletion(_ref3){let{state:state,element:element}=_ref3;if(this.reactive.isEditing||!element.istrackeduser)return;const completionElement=this.getElement(this.selectors.CM_COMPLETION);if(completionElement.dataset.value==element.completionstate)return;const data=this.reactive.getExporter().cmCompletion(state,element);try{const{html:html,js:js}=await _templates.default.renderForPromise("core_courseformat/local/courseindex/cmcompletion",data);_templates.default.replaceNode(completionElement,html,js)}catch(error){throw error}}_activityAnchor(event){const cm=this.reactive.get("cm",this.id);if(document.getElementById(cm.anchor))return void setTimeout((()=>{this.reactive.dispatch("setPageItem","cm",cm.id)}),50);const course=this.reactive.get("course"),section=this.reactive.get("section",cm.sectionid);if(!section)return;const url="".concat(course.baseurl,"§ion=").concat(section.number,"#").concat(cm.anchor);event.preventDefault(),window.location=url}}return _exports.default=Component,_exports.default}));
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_dndcmitem=_interopRequireDefault(_dndcmitem),_templates=_interopRequireDefault(_templates),_prefetch=_interopRequireDefault(_prefetch),_config=_interopRequireDefault(_config);_prefetch.default.prefetchTemplate("core_courseformat/local/courseindex/cmcompletion");class Component extends _dndcmitem.default{create(){this.name="courseindex_cm",this.selectors={CM_NAME:"[data-for='cm_name']",CM_COMPLETION:"[data-for='cm_completion']"},this.classes={CMHIDDEN:"dimmed",LOCKED:"editinprogress",RESTRICTIONS:"restrictions",PAGEITEM:"pageitem",INDENTED:"indented"},this.id=this.element.dataset.id}static init(target,selectors){return new Component({element:document.getElementById(target),selectors:selectors})}stateReady(state){this.configDragDrop(this.id);const cm=state.cm.get(this.id),course=state.course;this._refreshCompletion({state:state,element:cm});const anchor=new URL(window.location.href).hash.replace("#","");(window.location.href==cm.url||window.location.href.includes(course.baseurl)&&anchor==cm.anchor)&&(this.reactive.dispatch("setPageItem","cm",this.id),this.element.scrollIntoView({block:"center"})),_config.default.contextid!=_config.default.courseContextId&&_config.default.contextInstanceId==this.id&&(this.reactive.dispatch("setPageItem","cm",this.id,!0),this.element.scrollIntoView({block:"center"})),cm.uservisible||this.addEventListener(this.getElement(this.selectors.CM_NAME),"click",this._activityAnchor)}getWatchers(){return[{watch:"cm[".concat(this.id,"]:deleted"),handler:this.remove},{watch:"cm[".concat(this.id,"]:updated"),handler:this._refreshCm},{watch:"cm[".concat(this.id,"].completionstate:updated"),handler:this._refreshCompletion},{watch:"course.pageItem:updated",handler:this._refreshPageItem}]}_refreshCm(_ref){var _element$dragging,_element$locked,_element$hascmrestric;let{element:element}=_ref;this.element.classList.toggle(this.classes.CMHIDDEN,!element.visible),this.getElement(this.selectors.CM_NAME).innerHTML=element.name,this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.element.classList.toggle(this.classes.RESTRICTIONS,null!==(_element$hascmrestric=element.hascmrestrictions)&&void 0!==_element$hascmrestric&&_element$hascmrestric),this.element.classList.toggle(this.classes.INDENTED,element.indent),this.locked=element.locked}_refreshPageItem(_ref2){let{element:element}=_ref2;if(!element.pageItem)return;const isPageId="cm"==element.pageItem.type&&element.pageItem.id==this.id;this.element.classList.toggle(this.classes.PAGEITEM,isPageId),isPageId&&!this.reactive.isEditing&&this.element.scrollIntoView({block:"nearest"})}async _refreshCompletion(_ref3){let{state:state,element:element}=_ref3;if(this.reactive.isEditing||!element.istrackeduser)return;const completionElement=this.getElement(this.selectors.CM_COMPLETION);if(completionElement.dataset.value==element.completionstate)return;const data=this.reactive.getExporter().cmCompletion(state,element),{html:html,js:js}=await _templates.default.renderForPromise("core_courseformat/local/courseindex/cmcompletion",data);_templates.default.replaceNode(completionElement,html,js)}_activityAnchor(event){const cm=this.reactive.get("cm",this.id);if(document.getElementById(cm.anchor))return void setTimeout((()=>{this.reactive.dispatch("setPageItem","cm",cm.id)}),50);const course=this.reactive.get("course"),section=this.reactive.get("section",cm.sectionid);if(!section)return;const url="".concat(course.baseurl,"§ion=").concat(section.number,"#").concat(cm.anchor);event.preventDefault(),window.location=url}}return _exports.default=Component,_exports.default}));
|
||||
|
||||
//# sourceMappingURL=cm.min.js.map
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -173,15 +173,12 @@ export default class Component extends BaseComponent {
|
||||
const toggler = section.querySelector(this.selectors.COLLAPSE);
|
||||
const isCollapsed = toggler?.classList.contains(this.classes.COLLAPSED) ?? false;
|
||||
|
||||
if (isChevron || isCollapsed) {
|
||||
// Update the state.
|
||||
const sectionId = section.getAttribute('data-id');
|
||||
this.reactive.dispatch(
|
||||
'sectionContentCollapsed',
|
||||
[sectionId],
|
||||
!isCollapsed
|
||||
);
|
||||
}
|
||||
const sectionId = section.getAttribute('data-id');
|
||||
this.reactive.dispatch(
|
||||
'sectionContentCollapsed',
|
||||
[sectionId],
|
||||
!isCollapsed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,549 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* The course file uploader.
|
||||
*
|
||||
* This module is used to upload files directly into the course.
|
||||
*
|
||||
* @module core_courseformat/local/courseeditor/fileuploader
|
||||
* @copyright 2022 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Handler
|
||||
* @property {String} extension the handled extension or * for any
|
||||
* @property {String} message the handler message
|
||||
* @property {String} module the module name
|
||||
*/
|
||||
|
||||
import Config from 'core/config';
|
||||
import ModalFactory from 'core/modal_factory';
|
||||
import ModalEvents from 'core/modal_events';
|
||||
import Templates from 'core/templates';
|
||||
import {getFirst} from 'core/normalise';
|
||||
import {prefetchStrings} from 'core/prefetch';
|
||||
import {get_string as getString, get_strings as getStrings} from 'core/str';
|
||||
import {getCourseEditor} from 'core_courseformat/courseeditor';
|
||||
import {processMonitor} from 'core/process_monitor';
|
||||
import {debounce} from 'core/utils';
|
||||
|
||||
// Uploading url.
|
||||
const UPLOADURL = Config.wwwroot + '/course/dndupload.php';
|
||||
const DEBOUNCETIMER = 500;
|
||||
const USERCANIGNOREFILESIZELIMITS = -1;
|
||||
|
||||
/** @var {ProcessQueue} uploadQueue the internal uploadQueue instance. */
|
||||
let uploadQueue = null;
|
||||
/** @var {Object} handlerManagers the courseId indexed loaded handler managers. */
|
||||
let handlerManagers = {};
|
||||
/** @var {Map} courseUpdates the pending course sections updates. */
|
||||
let courseUpdates = new Map();
|
||||
/** @var {Object} errors the error messages. */
|
||||
let errors = null;
|
||||
|
||||
// Load global strings.
|
||||
prefetchStrings('moodle', ['addresourceoractivity', 'upload']);
|
||||
prefetchStrings('core_error', ['dndmaxbytes', 'dndread', 'dndupload', 'dndunkownfile']);
|
||||
|
||||
/**
|
||||
* Class to upload a file into the course.
|
||||
* @private
|
||||
*/
|
||||
class FileUploader {
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param {number} courseId the course id
|
||||
* @param {number} sectionId the section id
|
||||
* @param {number} sectionNum the section number
|
||||
* @param {File} fileInfo the file information object
|
||||
* @param {Handler} handler the file selected file handler
|
||||
*/
|
||||
constructor(courseId, sectionId, sectionNum, fileInfo, handler) {
|
||||
this.courseId = courseId;
|
||||
this.sectionId = sectionId;
|
||||
this.sectionNum = sectionNum;
|
||||
this.fileInfo = fileInfo;
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the file upload and update the state in the given process.
|
||||
*
|
||||
* @param {LoadingProcess} process the process to store the upload result
|
||||
*/
|
||||
execute(process) {
|
||||
const fileInfo = this.fileInfo;
|
||||
const xhr = this._createXhrRequest(process);
|
||||
const formData = this._createUploadFormData();
|
||||
|
||||
// Try reading the file to check it is not a folder, before sending it to the server.
|
||||
const reader = new FileReader();
|
||||
reader.onload = function() {
|
||||
// File was read OK - send it to the server.
|
||||
xhr.open("POST", UPLOADURL, true);
|
||||
xhr.send(formData);
|
||||
};
|
||||
reader.onerror = function() {
|
||||
// Unable to read the file (it is probably a folder) - display an error message.
|
||||
process.setError(errors.dndread);
|
||||
};
|
||||
if (fileInfo.size > 0) {
|
||||
// If this is a non-empty file, try reading the first few bytes.
|
||||
// This will trigger reader.onerror() for folders and reader.onload() for ordinary, readable files.
|
||||
reader.readAsText(fileInfo.slice(0, 5));
|
||||
} else {
|
||||
// If you call slice() on a 0-byte folder, before calling readAsText, then Firefox triggers reader.onload(),
|
||||
// instead of reader.onerror().
|
||||
// So, for 0-byte files, just call readAsText on the whole file (and it will trigger load/error functions as expected).
|
||||
reader.readAsText(fileInfo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the bind version of execute function.
|
||||
*
|
||||
* This method is used to queue the process into a ProcessQueue instance.
|
||||
*
|
||||
* @returns {Function} the bind function to execute the process
|
||||
*/
|
||||
getExecutionFunction() {
|
||||
return this.execute.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a upload XHR file request.
|
||||
*
|
||||
* @param {LoadingProcess} process the current process
|
||||
* @return {XMLHttpRequest} the XHR request
|
||||
*/
|
||||
_createXhrRequest(process) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
// Update the progress bar as the file is uploaded.
|
||||
xhr.upload.addEventListener(
|
||||
'progress',
|
||||
(event) => {
|
||||
if (event.lengthComputable) {
|
||||
const percent = Math.round((event.loaded * 100) / event.total);
|
||||
process.setPercentage(percent);
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
// Wait for the AJAX call to complete.
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState == 1) {
|
||||
// Add a 1% just to indicate that it is uploading.
|
||||
process.setPercentage(1);
|
||||
}
|
||||
// State 4 is DONE. Otherwise the connection is still ongoing.
|
||||
if (xhr.readyState != 4) {
|
||||
return;
|
||||
}
|
||||
if (xhr.status == 200) {
|
||||
var result = JSON.parse(xhr.responseText);
|
||||
if (result && result.error == 0) {
|
||||
// All OK.
|
||||
this._finishProcess(process);
|
||||
} else {
|
||||
process.setError(result.error);
|
||||
}
|
||||
} else {
|
||||
process.setError(errors.dndupload);
|
||||
}
|
||||
};
|
||||
return xhr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file into the course.
|
||||
*
|
||||
* @return {FormData|null} the new form data object
|
||||
*/
|
||||
_createUploadFormData() {
|
||||
const formData = new FormData();
|
||||
try {
|
||||
formData.append('repo_upload_file', this.fileInfo);
|
||||
} catch (error) {
|
||||
throw Error(error.dndread);
|
||||
}
|
||||
formData.append('sesskey', Config.sesskey);
|
||||
formData.append('course', this.courseId);
|
||||
formData.append('section', this.sectionNum);
|
||||
formData.append('module', this.handler.module);
|
||||
formData.append('type', 'Files');
|
||||
return formData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finishes the current process.
|
||||
* @param {LoadingProcess} process the process
|
||||
*/
|
||||
_finishProcess(process) {
|
||||
addRefreshSection(this.courseId, this.sectionId);
|
||||
process.setPercentage(100);
|
||||
process.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The file handler manager class.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
class HandlerManager {
|
||||
|
||||
/** @var {Object} lastHandlers the last handlers selected per each file extension. */
|
||||
lastHandlers = {};
|
||||
|
||||
/** @var {Handler[]|null} allHandlers all the available handlers. */
|
||||
allHandlers = null;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param {Number} courseId
|
||||
*/
|
||||
constructor(courseId) {
|
||||
this.courseId = courseId;
|
||||
this.lastUploadId = 0;
|
||||
this.courseEditor = getCourseEditor(courseId);
|
||||
if (!this.courseEditor) {
|
||||
throw Error('Unkown course editor');
|
||||
}
|
||||
this.maxbytes = this.courseEditor.get('course')?.maxbytes ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the course file handlers.
|
||||
*/
|
||||
async loadHandlers() {
|
||||
this.allHandlers = await this.courseEditor.getFileHandlersPromise();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the file extension from a fileInfo.
|
||||
*
|
||||
* @param {File} fileInfo
|
||||
* @returns {String} the file extension or an empty string.
|
||||
*/
|
||||
getFileExtension(fileInfo) {
|
||||
let extension = '';
|
||||
const dotpos = fileInfo.name.lastIndexOf('.');
|
||||
if (dotpos != -1) {
|
||||
extension = fileInfo.name.substring(dotpos + 1, fileInfo.name.length).toLowerCase();
|
||||
}
|
||||
return extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the file is valid.
|
||||
*
|
||||
* @param {File} fileInfo the file info
|
||||
*/
|
||||
validateFile(fileInfo) {
|
||||
if (this.maxbytes !== USERCANIGNOREFILESIZELIMITS && fileInfo.size > this.maxbytes) {
|
||||
throw Error(errors.dndmaxbytes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file handlers of an specific file.
|
||||
*
|
||||
* @param {File} fileInfo the file indo
|
||||
* @return {Array} Array of handlers
|
||||
*/
|
||||
filterHandlers(fileInfo) {
|
||||
const extension = this.getFileExtension(fileInfo);
|
||||
return this.allHandlers.filter(handler => handler.extension == '*' || handler.extension == extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Handler to upload a specific file.
|
||||
*
|
||||
* It will ask the used if more than one handler is available.
|
||||
*
|
||||
* @param {File} fileInfo the file info
|
||||
* @returns {Promise<Handler|null>} the selected handler or null if the user cancel
|
||||
*/
|
||||
async getFileHandler(fileInfo) {
|
||||
const fileHandlers = this.filterHandlers(fileInfo);
|
||||
if (fileHandlers.length == 0) {
|
||||
throw Error(errors.dndunkownfile);
|
||||
}
|
||||
let fileHandler = null;
|
||||
if (fileHandlers.length == 1) {
|
||||
fileHandler = fileHandlers[0];
|
||||
} else {
|
||||
fileHandler = await this.askHandlerToUser(fileHandlers, fileInfo);
|
||||
}
|
||||
return fileHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the user to select a specific handler.
|
||||
*
|
||||
* @param {Handler[]} fileHandlers
|
||||
* @param {File} fileInfo the file info
|
||||
* @return {Promise<Handler>} the selected handler
|
||||
*/
|
||||
async askHandlerToUser(fileHandlers, fileInfo) {
|
||||
const extension = this.getFileExtension(fileInfo);
|
||||
// Build the modal parameters from the event data.
|
||||
const modalParams = {
|
||||
title: getString('addresourceoractivity', 'moodle'),
|
||||
body: Templates.render(
|
||||
'core_courseformat/fileuploader',
|
||||
this.getModalData(
|
||||
fileHandlers,
|
||||
fileInfo,
|
||||
this.lastHandlers[extension] ?? null
|
||||
)
|
||||
),
|
||||
type: ModalFactory.types.SAVE_CANCEL,
|
||||
saveButtonText: getString('upload', 'moodle'),
|
||||
};
|
||||
// Create the modal.
|
||||
const modal = await this.modalBodyRenderedPromise(modalParams);
|
||||
const selectedHandler = await this.modalUserAnswerPromise(modal, fileHandlers);
|
||||
// Cancel action.
|
||||
if (selectedHandler === null) {
|
||||
return null;
|
||||
}
|
||||
// Save last selected handler.
|
||||
this.lastHandlers[extension] = selectedHandler.module;
|
||||
return selectedHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generated the modal template data.
|
||||
*
|
||||
* @param {Handler[]} fileHandlers
|
||||
* @param {File} fileInfo the file info
|
||||
* @param {String|null} defaultModule the default module if any
|
||||
* @return {Object} the modal template data.
|
||||
*/
|
||||
getModalData(fileHandlers, fileInfo, defaultModule) {
|
||||
const data = {
|
||||
filename: fileInfo.name,
|
||||
uploadid: ++this.lastUploadId,
|
||||
handlers: [],
|
||||
};
|
||||
let hasDefault = false;
|
||||
fileHandlers.forEach((handler, index) => {
|
||||
const isDefault = (defaultModule == handler.module);
|
||||
data.handlers.push({
|
||||
...handler,
|
||||
selected: isDefault,
|
||||
labelid: `fileuploader_${data.uploadid}`,
|
||||
value: index,
|
||||
});
|
||||
hasDefault = hasDefault || isDefault;
|
||||
});
|
||||
if (!hasDefault && data.handlers.length > 0) {
|
||||
const lastHandler = data.handlers.pop();
|
||||
lastHandler.selected = true;
|
||||
data.handlers.push(lastHandler);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user handler choice.
|
||||
*
|
||||
* Wait for the user answer in the modal and resolve with the selected index.
|
||||
*
|
||||
* @param {Modal} modal the modal instance
|
||||
* @param {Handler[]} fileHandlers the availabvle file handlers
|
||||
* @return {Promise} with the option selected by the user.
|
||||
*/
|
||||
modalUserAnswerPromise(modal, fileHandlers) {
|
||||
const modalBody = getFirst(modal.getBody());
|
||||
return new Promise((resolve, reject) => {
|
||||
modal.getRoot().on(
|
||||
ModalEvents.save,
|
||||
event => {
|
||||
// Get the selected option.
|
||||
const index = modalBody.querySelector('input:checked').value;
|
||||
event.preventDefault();
|
||||
modal.destroy();
|
||||
if (!fileHandlers[index]) {
|
||||
reject('Invalid handler selected');
|
||||
}
|
||||
resolve(fileHandlers[index]);
|
||||
|
||||
}
|
||||
);
|
||||
modal.getRoot().on(
|
||||
ModalEvents.cancel,
|
||||
() => {
|
||||
resolve(null);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new modal and return a Promise to the body rendered.
|
||||
*
|
||||
* @param {Object} modalParams the modal params
|
||||
* @returns {Promise} the modal body rendered promise
|
||||
*/
|
||||
modalBodyRenderedPromise(modalParams) {
|
||||
return new Promise((resolve, reject) => {
|
||||
ModalFactory.create(modalParams).then((modal) => {
|
||||
modal.setRemoveOnClose(true);
|
||||
// Handle body loading event.
|
||||
modal.getRoot().on(ModalEvents.bodyRendered, () => {
|
||||
resolve(modal);
|
||||
});
|
||||
// Configure some extra modal params.
|
||||
if (modalParams.saveButtonText !== undefined) {
|
||||
modal.setSaveButtonText(modalParams.saveButtonText);
|
||||
}
|
||||
modal.show();
|
||||
return;
|
||||
}).catch(() => {
|
||||
reject(`Cannot load modal content`);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a section to refresh.
|
||||
*
|
||||
* @param {number} courseId the course id
|
||||
* @param {number} sectionId the seciton id
|
||||
*/
|
||||
function addRefreshSection(courseId, sectionId) {
|
||||
let refresh = courseUpdates.get(courseId);
|
||||
if (!refresh) {
|
||||
refresh = new Set();
|
||||
}
|
||||
refresh.add(sectionId);
|
||||
courseUpdates.set(courseId, refresh);
|
||||
refreshCourseEditors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounced processing all pending course refreshes.
|
||||
* @private
|
||||
*/
|
||||
const refreshCourseEditors = debounce(
|
||||
() => {
|
||||
const refreshes = courseUpdates;
|
||||
courseUpdates = new Map();
|
||||
refreshes.forEach((sectionIds, courseId) => {
|
||||
const courseEditor = getCourseEditor(courseId);
|
||||
if (!courseEditor) {
|
||||
return;
|
||||
}
|
||||
courseEditor.dispatch('sectionState', [...sectionIds]);
|
||||
});
|
||||
},
|
||||
DEBOUNCETIMER
|
||||
);
|
||||
|
||||
/**
|
||||
* Load and return the course handler manager instance.
|
||||
*
|
||||
* @param {Number} courseId the course Id to load
|
||||
* @returns {Promise<HandlerManager>} promise of the the loaded handleManager
|
||||
*/
|
||||
async function loadCourseHandlerManager(courseId) {
|
||||
if (handlerManagers[courseId] !== undefined) {
|
||||
return handlerManagers[courseId];
|
||||
}
|
||||
const handlerManager = new HandlerManager(courseId);
|
||||
await handlerManager.loadHandlers();
|
||||
handlerManagers[courseId] = handlerManager;
|
||||
return handlerManagers[courseId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all the erros messages at once in the module "errors" variable.
|
||||
* @param {Number} courseId the course id
|
||||
*/
|
||||
async function loadErrorStrings(courseId) {
|
||||
if (errors !== null) {
|
||||
return;
|
||||
}
|
||||
const courseEditor = getCourseEditor(courseId);
|
||||
const maxbytestext = courseEditor.get('course')?.maxbytestext ?? '0';
|
||||
|
||||
errors = {};
|
||||
const allStrings = [
|
||||
{key: 'dndmaxbytes', component: 'core_error', param: {size: maxbytestext}},
|
||||
{key: 'dndread', component: 'core_error'},
|
||||
{key: 'dndupload', component: 'core_error'},
|
||||
{key: 'dndunkownfile', component: 'core_error'},
|
||||
];
|
||||
window.console.log(allStrings);
|
||||
const loadedStrings = await getStrings(allStrings);
|
||||
allStrings.forEach(({key}, index) => {
|
||||
errors[key] = loadedStrings[index];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a batch file uploading into the course.
|
||||
*
|
||||
* @private
|
||||
* @param {number} courseId the course id.
|
||||
* @param {number} sectionId the section id.
|
||||
* @param {number} sectionNum the section number.
|
||||
* @param {File} fileInfo the file information object
|
||||
* @param {HandlerManager} handlerManager the course handler manager
|
||||
*/
|
||||
const queueFileUpload = async function(courseId, sectionId, sectionNum, fileInfo, handlerManager) {
|
||||
let handler;
|
||||
uploadQueue = await processMonitor.createProcessQueue();
|
||||
try {
|
||||
handlerManager.validateFile(fileInfo);
|
||||
handler = await handlerManager.getFileHandler(fileInfo);
|
||||
} catch (error) {
|
||||
uploadQueue.addError(fileInfo.name, error.message);
|
||||
return;
|
||||
}
|
||||
// If we don't have a handler means the user cancel the upload.
|
||||
if (!handler) {
|
||||
return;
|
||||
}
|
||||
const fileProcessor = new FileUploader(courseId, sectionId, sectionNum, fileInfo, handler);
|
||||
uploadQueue.addPending(fileInfo.name, fileProcessor.getExecutionFunction());
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload a file to the course.
|
||||
*
|
||||
* This method will show any necesary modal to handle the request.
|
||||
*
|
||||
* @param {number} courseId the course id
|
||||
* @param {number} sectionId the section id
|
||||
* @param {number} sectionNum the section number
|
||||
* @param {Array} files and array of files
|
||||
*/
|
||||
export const uploadFilesToCourse = async function(courseId, sectionId, sectionNum, files) {
|
||||
// Get the course handlers.
|
||||
const handlerManager = await loadCourseHandlerManager(courseId);
|
||||
await loadErrorStrings(courseId);
|
||||
for (let index = 0; index < files.length; index++) {
|
||||
const fileInfo = files[index];
|
||||
await queueFileUpload(courseId, sectionId, sectionNum, fileInfo, handlerManager);
|
||||
}
|
||||
};
|
||||
@@ -473,12 +473,16 @@ export default class {
|
||||
* @param {boolean} collapsed the new collapsed value
|
||||
*/
|
||||
async sectionIndexCollapsed(stateManager, sectionIds, collapsed) {
|
||||
const collapsedIds = this._updateStateSectionPreference(stateManager, 'indexcollapsed', sectionIds, collapsed);
|
||||
if (!collapsedIds) {
|
||||
const affectedSections = this._updateStateSectionPreference(stateManager, 'indexcollapsed', sectionIds, collapsed);
|
||||
if (!affectedSections) {
|
||||
return;
|
||||
}
|
||||
const course = stateManager.get('course');
|
||||
await this._callEditWebservice('section_index_collapsed', course.id, collapsedIds);
|
||||
let actionName = 'section_index_collapsed';
|
||||
if (!collapsed) {
|
||||
actionName = 'section_index_expanded';
|
||||
}
|
||||
await this._callEditWebservice(actionName, course.id, affectedSections);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -489,12 +493,16 @@ export default class {
|
||||
* @param {boolean} collapsed the new collapsed value
|
||||
*/
|
||||
async sectionContentCollapsed(stateManager, sectionIds, collapsed) {
|
||||
const collapsedIds = this._updateStateSectionPreference(stateManager, 'contentcollapsed', sectionIds, collapsed);
|
||||
if (!collapsedIds) {
|
||||
const affectedSections = this._updateStateSectionPreference(stateManager, 'contentcollapsed', sectionIds, collapsed);
|
||||
if (!affectedSections) {
|
||||
return;
|
||||
}
|
||||
const course = stateManager.get('course');
|
||||
await this._callEditWebservice('section_content_collapsed', course.id, collapsedIds);
|
||||
let actionName = 'section_content_collapsed';
|
||||
if (!collapsed) {
|
||||
actionName = 'section_content_expanded';
|
||||
}
|
||||
await this._callEditWebservice(actionName, course.id, affectedSections);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -508,32 +516,22 @@ export default class {
|
||||
*/
|
||||
_updateStateSectionPreference(stateManager, preferenceName, sectionIds, preferenceValue) {
|
||||
stateManager.setReadOnly(false);
|
||||
const affectedSections = new Set();
|
||||
const affectedSections = [];
|
||||
// Check if we need to update preferences.
|
||||
sectionIds.forEach(sectionId => {
|
||||
const section = stateManager.get('section', sectionId);
|
||||
if (section === undefined) {
|
||||
stateManager.setReadOnly(true);
|
||||
return null;
|
||||
}
|
||||
const newValue = preferenceValue ?? section[preferenceName];
|
||||
if (section[preferenceName] != newValue) {
|
||||
section[preferenceName] = newValue;
|
||||
affectedSections.add(section.id);
|
||||
affectedSections.push(section.id);
|
||||
}
|
||||
});
|
||||
stateManager.setReadOnly(true);
|
||||
if (affectedSections.size == 0) {
|
||||
return null;
|
||||
}
|
||||
// Get all collapsed section ids.
|
||||
const collapsedSectionIds = [];
|
||||
const state = stateManager.state;
|
||||
state.section.forEach(section => {
|
||||
if (section[preferenceName]) {
|
||||
collapsedSectionIds.push(section.id);
|
||||
}
|
||||
});
|
||||
return collapsedSectionIds;
|
||||
return affectedSections;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -180,12 +180,8 @@ export default class Component extends DndCmItem {
|
||||
const exporter = this.reactive.getExporter();
|
||||
const data = exporter.cmCompletion(state, element);
|
||||
|
||||
try {
|
||||
const {html, js} = await Templates.renderForPromise(completionTemplate, data);
|
||||
Templates.replaceNode(completionElement, html, js);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
const {html, js} = await Templates.renderForPromise(completionTemplate, data);
|
||||
Templates.replaceNode(completionElement, html, js);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -131,9 +131,16 @@ export default class Component extends BaseComponent {
|
||||
const toggler = section.querySelector(this.selectors.COLLAPSE);
|
||||
const isCollapsed = toggler?.classList.contains(this.classes.COLLAPSED) ?? false;
|
||||
|
||||
if (isChevron || isCollapsed) {
|
||||
// Update the state.
|
||||
const sectionId = section.getAttribute('data-id');
|
||||
// Update the state.
|
||||
const sectionId = section.getAttribute('data-id');
|
||||
if ((!sectionlink)) {
|
||||
this.reactive.dispatch(
|
||||
'sectionIndexCollapsed',
|
||||
[sectionId],
|
||||
!isCollapsed
|
||||
);
|
||||
} else if (isCollapsed) {
|
||||
// Always expand the section when clicking on section name.
|
||||
this.reactive.dispatch(
|
||||
'sectionIndexCollapsed',
|
||||
[sectionId],
|
||||
|
||||
@@ -617,8 +617,9 @@ abstract class base {
|
||||
global $USER;
|
||||
$course = $this->get_course();
|
||||
try {
|
||||
$sectionpreferences = (array) json_decode(
|
||||
get_user_preferences("coursesectionspreferences_{$course->id}", '', $USER->id)
|
||||
$sectionpreferences = json_decode(
|
||||
get_user_preferences("coursesectionspreferences_{$course->id}", '', $USER->id),
|
||||
true
|
||||
);
|
||||
if (empty($sectionpreferences)) {
|
||||
$sectionpreferences = [];
|
||||
@@ -637,10 +638,65 @@ abstract class base {
|
||||
*
|
||||
*/
|
||||
public function set_sections_preference(string $preferencename, array $sectionids) {
|
||||
global $USER;
|
||||
$course = $this->get_course();
|
||||
$sectionpreferences = $this->get_sections_preferences_by_preference();
|
||||
$sectionpreferences[$preferencename] = $sectionids;
|
||||
$this->persist_to_user_preference($sectionpreferences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add section preference ids.
|
||||
*
|
||||
* @param string $preferencename preference name
|
||||
* @param array $sectionids affected section ids
|
||||
*/
|
||||
public function add_section_preference_ids(
|
||||
string $preferencename,
|
||||
array $sectionids
|
||||
): void {
|
||||
$sectionpreferences = $this->get_sections_preferences_by_preference();
|
||||
if (!isset($sectionpreferences[$preferencename])) {
|
||||
$sectionpreferences[$preferencename] = [];
|
||||
}
|
||||
foreach ($sectionids as $sectionid) {
|
||||
if (!in_array($sectionid, $sectionpreferences[$preferencename])) {
|
||||
$sectionpreferences[$preferencename][] = $sectionid;
|
||||
}
|
||||
}
|
||||
$this->persist_to_user_preference($sectionpreferences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove section preference ids.
|
||||
*
|
||||
* @param string $preferencename preference name
|
||||
* @param array $sectionids affected section ids
|
||||
*/
|
||||
public function remove_section_preference_ids(
|
||||
string $preferencename,
|
||||
array $sectionids
|
||||
): void {
|
||||
$sectionpreferences = $this->get_sections_preferences_by_preference();
|
||||
if (!isset($sectionpreferences[$preferencename])) {
|
||||
$sectionpreferences[$preferencename] = [];
|
||||
}
|
||||
foreach ($sectionids as $sectionid) {
|
||||
if (($key = array_search($sectionid, $sectionpreferences[$preferencename])) !== false) {
|
||||
unset($sectionpreferences[$preferencename][$key]);
|
||||
}
|
||||
}
|
||||
$this->persist_to_user_preference($sectionpreferences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the section preferences to the user preferences.
|
||||
*
|
||||
* @param array $sectionpreferences the section preferences
|
||||
*/
|
||||
private function persist_to_user_preference(
|
||||
array $sectionpreferences
|
||||
): void {
|
||||
global $USER;
|
||||
$course = $this->get_course();
|
||||
set_user_preference('coursesectionspreferences_' . $course->id, json_encode($sectionpreferences), $USER->id);
|
||||
// Invalidate section preferences cache.
|
||||
$coursesectionscache = cache::make('core', 'coursesectionspreferences');
|
||||
|
||||
@@ -531,7 +531,7 @@ class stateactions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the course content section collapsed value.
|
||||
* Update the course content section state to collapse.
|
||||
*
|
||||
* @param stateupdates $updates the affected course elements track
|
||||
* @param stdClass $course the course object
|
||||
@@ -550,11 +550,34 @@ class stateactions {
|
||||
$this->validate_sections($course, $ids, __FUNCTION__);
|
||||
}
|
||||
$format = course_get_format($course->id);
|
||||
$format->set_sections_preference('contentcollapsed', $ids);
|
||||
$format->add_section_preference_ids('contentcollapsed', $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the course index section collapsed value.
|
||||
* Update the course content section state to expand.
|
||||
*
|
||||
* @param stateupdates $updates the affected course elements track
|
||||
* @param stdClass $course the course object
|
||||
* @param int[] $ids the collapsed section ids
|
||||
* @param int|null $targetsectionid not used
|
||||
* @param int|null $targetcmid not used
|
||||
*/
|
||||
public function section_content_expanded(
|
||||
stateupdates $updates,
|
||||
stdClass $course,
|
||||
array $ids = [],
|
||||
?int $targetsectionid = null,
|
||||
?int $targetcmid = null
|
||||
): void {
|
||||
if (!empty($ids)) {
|
||||
$this->validate_sections($course, $ids, __FUNCTION__);
|
||||
}
|
||||
$format = course_get_format($course->id);
|
||||
$format->remove_section_preference_ids('contentcollapsed', $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the course index section state to collapse.
|
||||
*
|
||||
* @param stateupdates $updates the affected course elements track
|
||||
* @param stdClass $course the course object
|
||||
@@ -573,7 +596,30 @@ class stateactions {
|
||||
$this->validate_sections($course, $ids, __FUNCTION__);
|
||||
}
|
||||
$format = course_get_format($course->id);
|
||||
$format->set_sections_preference('indexcollapsed', $ids);
|
||||
$format->add_section_preference_ids('indexcollapsed', $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the course index section state to expand.
|
||||
*
|
||||
* @param stateupdates $updates the affected course elements track
|
||||
* @param stdClass $course the course object
|
||||
* @param int[] $ids the collapsed section ids
|
||||
* @param int $targetsectionid not used
|
||||
* @param int $targetcmid not used
|
||||
*/
|
||||
public function section_index_expanded(
|
||||
stateupdates $updates,
|
||||
stdClass $course,
|
||||
array $ids = [],
|
||||
?int $targetsectionid = null,
|
||||
?int $targetcmid = null
|
||||
): void {
|
||||
if (!empty($ids)) {
|
||||
$this->validate_sections($course, $ids, __FUNCTION__);
|
||||
}
|
||||
$format = course_get_format($course->id);
|
||||
$format->remove_section_preference_ids('indexcollapsed', $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -347,6 +347,73 @@ class base_test extends advanced_testcase {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test add_section_preference_ids() method.
|
||||
*
|
||||
* @covers \core_courseformat\base::persist_to_user_preference
|
||||
*/
|
||||
public function test_add_section_preference_ids(): void {
|
||||
$this->resetAfterTest();
|
||||
// Create initial data.
|
||||
$generator = $this->getDataGenerator();
|
||||
$course = $generator->create_course();
|
||||
$user = $generator->create_and_enrol($course);
|
||||
// Get the course format.
|
||||
$format = course_get_format($course);
|
||||
// Login as the user.
|
||||
$this->setUser($user);
|
||||
|
||||
// Add section preference ids.
|
||||
$format->add_section_preference_ids('pref1', [1, 2]);
|
||||
$format->add_section_preference_ids('pref1', [3]);
|
||||
$format->add_section_preference_ids('pref2', [1]);
|
||||
|
||||
// Get section preferences.
|
||||
$sectionpreferences = $format->get_sections_preferences_by_preference();
|
||||
$this->assertCount(3, $sectionpreferences['pref1']);
|
||||
$this->assertContains(1, $sectionpreferences['pref1']);
|
||||
$this->assertContains(2, $sectionpreferences['pref1']);
|
||||
$this->assertContains(3, $sectionpreferences['pref1']);
|
||||
$this->assertCount(1, $sectionpreferences['pref2']);
|
||||
$this->assertContains(1, $sectionpreferences['pref1']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test remove_section_preference_ids() method.
|
||||
*
|
||||
* @covers \core_courseformat\base::persist_to_user_preference
|
||||
*/
|
||||
public function test_remove_section_preference_ids(): void {
|
||||
$this->resetAfterTest();
|
||||
// Create initial data.
|
||||
$generator = $this->getDataGenerator();
|
||||
$course = $generator->create_course();
|
||||
$user = $generator->create_and_enrol($course);
|
||||
// Get the course format.
|
||||
$format = course_get_format($course);
|
||||
// Login as the user.
|
||||
$this->setUser($user);
|
||||
// Set initial preferences.
|
||||
$format->set_sections_preference('pref1', [1, 2, 3]);
|
||||
$format->set_sections_preference('pref2', [1]);
|
||||
|
||||
// Remove section with id = 3 out of the pref1.
|
||||
$format->remove_section_preference_ids('pref1', [3]);
|
||||
// Get section preferences.
|
||||
$sectionpreferences = $format->get_sections_preferences_by_preference();
|
||||
$this->assertCount(2, $sectionpreferences['pref1']);
|
||||
$this->assertCount(1, $sectionpreferences['pref2']);
|
||||
|
||||
// Remove section with id = 2 out of the pref1.
|
||||
$format->remove_section_preference_ids('pref1', [2]);
|
||||
// Remove section with id = 1 out of the pref2.
|
||||
$format->remove_section_preference_ids('pref2', [1]);
|
||||
// Get section preferences.
|
||||
$sectionpreferences = $format->get_sections_preferences_by_preference();
|
||||
$this->assertCount(1, $sectionpreferences['pref1']);
|
||||
$this->assertEmpty($sectionpreferences['pref2']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that retrieving last section number for a course
|
||||
*
|
||||
|
||||
@@ -2,6 +2,10 @@ This files describes API changes for course formats
|
||||
|
||||
Overview of this plugin type at https://moodledev.io/docs/apis/plugintypes/format
|
||||
|
||||
=== 4.1.10 ===
|
||||
* New core_courseformat\base::add_section_preference_ids() to add the section ids to course format preferences.
|
||||
* New core_courseformat\base::remove_section_preference_ids() to remove the section ids out of course format preferences.
|
||||
|
||||
=== 4.1 ===
|
||||
* New \core_courseformat\stateupdates methods add_section_remove() and add_cm_remove() have been added to replace
|
||||
the deprecated methods add_section_delete() and add_cm_delete().
|
||||
|
||||
@@ -9,11 +9,13 @@ Feature: Display and choose from the available activities in course
|
||||
| username | firstname | lastname | email |
|
||||
| teacher | Teacher | 1 | teacher@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | format |
|
||||
| Course | C | topics |
|
||||
| fullname | shortname | format | startdate |
|
||||
| Course | C | topics | |
|
||||
| Course 2 | C2 | weeks | 95713920 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher | C | editingteacher |
|
||||
| user | course | role |
|
||||
| teacher | C | editingteacher |
|
||||
| teacher | C2 | editingteacher |
|
||||
And the following config values are set as admin:
|
||||
| enablemoodlenet | 0 | tool_moodlenet |
|
||||
And I log in as "teacher"
|
||||
@@ -44,6 +46,12 @@ Feature: Display and choose from the available activities in course
|
||||
When I click on "Information about the Assignment activity" "button" in the "Add an activity or resource" "dialogue"
|
||||
Then I should see "Assignment" in the "help" "core_course > Activity chooser screen"
|
||||
And I should see "The assignment activity module enables a teacher to communicate tasks, collect work and provide grades and feedback."
|
||||
# Confirm show summary also works for weekly format course
|
||||
And I am on "C2" course homepage with editing mode on
|
||||
And I click on "Add an activity or resource" "button" in the "13 January - 19 January" "section"
|
||||
And I click on "Information about the Assignment activity" "button" in the "Add an activity or resource" "dialogue"
|
||||
And I should see "Assignment" in the "help" "core_course > Activity chooser screen"
|
||||
And I should see "The assignment activity module enables a teacher to communicate tasks, collect work and provide grades and feedback."
|
||||
|
||||
Scenario: Hide summary
|
||||
Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
|
||||
@@ -55,6 +63,15 @@ Feature: Display and choose from the available activities in course
|
||||
And "help" "core_course > Activity chooser screen" should not be visible
|
||||
And "Back" "button" should not exist in the "modules" "core_course > Activity chooser screen"
|
||||
And I should not see "The assignment activity module enables a teacher to communicate tasks, collect work and provide grades and feedback." in the "Add an activity or resource" "dialogue"
|
||||
# Confirm hide summary also works for weekly format course
|
||||
And I am on "C2" course homepage with editing mode on
|
||||
And I click on "Add an activity or resource" "button" in the "13 January - 19 January" "section"
|
||||
And I click on "Information about the Assignment activity" "button" in the "Add an activity or resource" "dialogue"
|
||||
And I click on "Back" "button" in the "help" "core_course > Activity chooser screen"
|
||||
And "modules" "core_course > Activity chooser screen" should be visible
|
||||
And "help" "core_course > Activity chooser screen" should not be visible
|
||||
And "Back" "button" should not exist in the "modules" "core_course > Activity chooser screen"
|
||||
And I should not see "The assignment activity module enables a teacher to communicate tasks, collect work and provide grades and feedback." in the "Add an activity or resource" "dialogue"
|
||||
|
||||
Scenario: View recommended activities
|
||||
When I log out
|
||||
@@ -189,3 +206,69 @@ Feature: Display and choose from the available activities in course
|
||||
Then I should not see "All" in the "Add an activity or resource" "dialogue"
|
||||
And I should see "Activities" in the "Add an activity or resource" "dialogue"
|
||||
And I should see "Resources" in the "Add an activity or resource" "dialogue"
|
||||
|
||||
Scenario: Teacher can navigate through activity chooser in Topics format course
|
||||
When I click on "Add an activity or resource" "button" in the "Topic 1" "section"
|
||||
Then I should see "All" in the "Add an activity or resource" "dialogue"
|
||||
And I press the tab key
|
||||
And I press the tab key
|
||||
And I press the tab key
|
||||
And I press the tab key
|
||||
# Confirm right key works
|
||||
And I press the right key
|
||||
And I press the right key
|
||||
And the focused element is "Chat" "menuitem" in the "Add an activity or resource" "dialogue"
|
||||
# Confirm left key works
|
||||
And I press the left key
|
||||
And the focused element is "Book" "menuitem" in the "Add an activity or resource" "dialogue"
|
||||
# Confirm clicking "x" button closes modal
|
||||
And I click on "Close" "button" in the "Add an activity or resource" "dialogue"
|
||||
And "Add an activity or resource" "dialogue" should not be visible
|
||||
And I click on "Add an activity or resource" "button" in the "Topic 1" "section"
|
||||
# Confirm escape key closes the modal
|
||||
And I press the escape key
|
||||
And "Add an activity or resource" "dialogue" should not be visible
|
||||
|
||||
Scenario: Teacher can navigate through activity chooser in Weekly format course
|
||||
Given I am on "C2" course homepage with editing mode on
|
||||
When I click on "Add an activity or resource" "button" in the "13 January - 19 January" "section"
|
||||
Then I should see "All" in the "Add an activity or resource" "dialogue"
|
||||
And I press the tab key
|
||||
And I press the tab key
|
||||
And I press the tab key
|
||||
And I press the tab key
|
||||
# Confirm right key works
|
||||
And I press the right key
|
||||
And I press the right key
|
||||
And the focused element is "Chat" "menuitem" in the "Add an activity or resource" "dialogue"
|
||||
# Confirm left key works
|
||||
And I press the left key
|
||||
And the focused element is "Book" "menuitem" in the "Add an activity or resource" "dialogue"
|
||||
# Confirm clicking "x" button closes modal
|
||||
And I click on "Close" "button" in the "Add an activity or resource" "dialogue"
|
||||
And "Add an activity or resource" "dialogue" should not be visible
|
||||
And I click on "Add an activity or resource" "button" in the "13 January - 19 January" "section"
|
||||
# Confirm escape key closes the modal
|
||||
And I press the escape key
|
||||
And "Add an activity or resource" "dialogue" should not be visible
|
||||
|
||||
Scenario: Teacher can access 'More help' from activity information in activity chooser
|
||||
Given I click on "Add an activity or resource" "button" in the "Topic 1" "section"
|
||||
When I click on "Information about the Assignment activity" "button" in the "Add an activity or resource" "dialogue"
|
||||
# Confirm more help link exists
|
||||
Then "More help" "link" should exist
|
||||
# Confirm that corresponding help icon exist
|
||||
And ".iconhelp" "css_element" should exist
|
||||
# Confirm that link opens in new window
|
||||
And "Opens in new window" "link" should be visible
|
||||
# Confirm the same behaviour for weekly format course
|
||||
And I am on "C2" course homepage with editing mode on
|
||||
And I click on "Add an activity or resource" "button" in the "13 January - 19 January" "section"
|
||||
And I should see "All" in the "Add an activity or resource" "dialogue"
|
||||
And I click on "Information about the Assignment activity" "button" in the "Add an activity or resource" "dialogue"
|
||||
# Confirm more help link exists
|
||||
And "More help" "link" should exist
|
||||
# Confirm that corresponding help icon exist
|
||||
And ".iconhelp" "css_element" should exist
|
||||
# Confirm that link opens in new window
|
||||
And "Opens in new window" "link" should be visible
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
@core @core_course @core_tag
|
||||
Feature: Delete activity tags during course reset
|
||||
As an admin,
|
||||
I should be able to delete activity tags by performing course reset
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
And the following "activities" exist:
|
||||
| activity | name | course | idnumber |
|
||||
| book | Test Book | C1 | book1 |
|
||||
| forum | Test Forum | C1 | forum1 |
|
||||
| glossary | Test Glossary | C1 | glossary1 |
|
||||
|
||||
@javascript
|
||||
Scenario: Delete book chapter tags using course reset
|
||||
# Added multiple tags to confirm that all tags are deleted on course reset.
|
||||
Given the following "mod_book > chapters" exist:
|
||||
| book | title | content | tags |
|
||||
| Test Book | Chapter 1 | Chapter 1 content | SampleTag, ChapterTag |
|
||||
# Perform course reset without checking anything.
|
||||
And I log in as "admin"
|
||||
And I am on the "Course 1" "reset" page
|
||||
And I press "Reset"
|
||||
And I press "Continue"
|
||||
# Confirm that book chapter tags are not deleted.
|
||||
When I am on the "Test Book" "book activity" page
|
||||
Then I should see "SampleTag"
|
||||
And I should see "ChapterTag"
|
||||
# Delete book chapter tags using course reset.
|
||||
And I am on the "Course 1" "reset" page
|
||||
And I expand all fieldsets
|
||||
And I click on "Remove all book tags" "checkbox"
|
||||
And I press "Reset"
|
||||
# Confirm that book chapter tags are deleted.
|
||||
And I should see "Book tags have been deleted" in the "Books" "table_row"
|
||||
And I press "Continue"
|
||||
And I am on the "Test Book" "book activity" page
|
||||
And I should not see "SampleTag"
|
||||
And I should not see "ChapterTag"
|
||||
|
||||
@javascript
|
||||
Scenario Outline: Delete forum discussion tags using course reset
|
||||
Given the following "mod_forum > discussions" exist:
|
||||
| user | forum | name | message | tags |
|
||||
| admin | forum1 | Discussion 1 | Discussion 1 message | SampleTag, DiscussionTag |
|
||||
# Perform course reset without checking anything.
|
||||
And I am on the "Course 1" "reset" page logged in as admin
|
||||
And I press "Reset"
|
||||
And I press "Continue"
|
||||
# Confirm that forum discussion tags are not deleted.
|
||||
When I am on the "Test Forum" "forum activity" page
|
||||
And I follow "Discussion 1"
|
||||
Then I should see "SampleTag"
|
||||
And I should see "DiscussionTag"
|
||||
And I am on the "Course 1" "reset" page
|
||||
And I expand all fieldsets
|
||||
# Depending on <resetcheck> value, either delete all discussion posts or remove all forum discussion tags only.
|
||||
And I click on "<resetcheck>" "checkbox"
|
||||
# Confirm `Remove all forum tags` is disabled when `Delete all posts` on previous step is checked.
|
||||
And the "Remove all forum tags" "checkbox" should be <canbechecked>
|
||||
And I press "Reset"
|
||||
And I should see "<resetmessage>" in the "Forums" "table_row"
|
||||
And I press "Continue"
|
||||
And I am on the "Test Forum" "forum activity" page
|
||||
# Confirm discussion is deleted when `Delete all posts` was checked.
|
||||
And I <forumview> see "There are no discussion topics yet in this forum"
|
||||
# Confirm all discussion tags are deleted.
|
||||
And I should not see "SampleTag"
|
||||
And I should not see "DiscussionTag"
|
||||
|
||||
Examples:
|
||||
| resetcheck | resetmessage | canbechecked | forumview |
|
||||
| Delete all posts | Delete all posts | disabled | should |
|
||||
| Remove all forum tags | Forum tags have been deleted | enabled | should not |
|
||||
|
||||
@javascript
|
||||
Scenario Outline: Delete glossary entry tags using course reuse
|
||||
Given the following "mod_glossary > entries" exist:
|
||||
| glossary | concept | definition | user | tags |
|
||||
| Test Glossary | Aubergine | Also eggpgplant | admin | SampleTag, GlossaryTag |
|
||||
# Perform course reset without checking anything.
|
||||
And I am on the "Course 1" "reset" page logged in as admin
|
||||
And I press "Reset"
|
||||
And I press "Continue"
|
||||
# Confirm that glossary entry tags are not deleted.
|
||||
When I am on the "Test Glossary" "glossary activity" page
|
||||
Then I should see "SampleTag"
|
||||
And I should see "GlossaryTag"
|
||||
And I am on the "Course 1" "reset" page
|
||||
And I expand all fieldsets
|
||||
# Depending on <resetcheck> value, either delete all glossary entries or remove all glossary entry tags only.
|
||||
And I click on "<resetcheck>" "checkbox"
|
||||
# Confirm `Remove all forum tags` is disabled when `Delete entries from all glossaries` on previous step is checked.
|
||||
And the "Remove all glossary tags" "checkbox" should be <canbechecked>
|
||||
And I press "Reset"
|
||||
And I should see "<resetmessage>" in the "Glossaries" "table_row"
|
||||
And I press "Continue"
|
||||
And I am on the "Test Glossary" "glossary activity" page
|
||||
# Confirm glossary entries are deleted when `Delete entries from all glossaries` is checked.
|
||||
And I <glossaryview> see "No entries found in this section"
|
||||
# Confirm that glossary entry tags are deleted.
|
||||
And I should not see "SampleTag"
|
||||
And I should not see "GlossaryTag"
|
||||
|
||||
Examples:
|
||||
| resetcheck | resetmessage | canbechecked | glossaryview |
|
||||
| Delete entries from all glossaries | Delete entries from all glossaries | disabled | should |
|
||||
| Remove all glossary tags | Glossary tags have been deleted | enabled | should not |
|
||||
@@ -0,0 +1,38 @@
|
||||
@core_course
|
||||
Feature: Force course language
|
||||
To be able to force a language for the course
|
||||
As a teacher
|
||||
I need to be able to set force language setting for the course
|
||||
|
||||
Background:
|
||||
Given remote langimport tests are enabled
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | One | teacher1@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "language pack" exists:
|
||||
| language | fr | es |
|
||||
|
||||
Scenario: A teacher can set a course language
|
||||
Given I am on the "Course 1" course page logged in as "teacher1"
|
||||
# Site default language is English.
|
||||
And I should see "Settings"
|
||||
And I follow "Preferences" in the user menu
|
||||
And I follow "Preferred language"
|
||||
# Change preferred language to French.
|
||||
And I set the field "Preferred language" to "fr"
|
||||
And I press "Save changes"
|
||||
And I am on "Course 1" course homepage
|
||||
And I should see "Paramètres"
|
||||
# Change course language to Spanish.
|
||||
And I navigate to "Paramètres" in current page administration
|
||||
When I set the following fields to these values:
|
||||
| id_lang | es |
|
||||
And I press "Enregistrer et afficher"
|
||||
# Course language is now Spanish.
|
||||
Then I should see "Configuración"
|
||||
@@ -10,8 +10,8 @@ Feature: Show/hide course sections
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
| student1 | Student | 1 | student1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | format | hiddensections |
|
||||
| Course 1 | C1 | topics | 0 |
|
||||
| fullname | shortname | format | hiddensections | enablecompletion | coursedisplay |
|
||||
| Course 1 | C1 | topics | 0 | 1 | 1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
@@ -76,12 +76,7 @@ Feature: Show/hide course sections
|
||||
|
||||
@javascript
|
||||
Scenario: Students can not navigate to restricted sections
|
||||
Given I navigate to "Settings" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Course layout | Show one section per page |
|
||||
| Enable completion tracking | Yes |
|
||||
And I press "Save and display"
|
||||
And the following "activities" exist:
|
||||
Given the following "activities" exist:
|
||||
| activity | course | section | name | completion |
|
||||
| label | C1 | 1 | Test label | 1 |
|
||||
And I edit the section "2"
|
||||
|
||||
@@ -263,4 +263,19 @@ class renderer extends plugin_renderer_base {
|
||||
return parent::render_from_template('enrol_lti/local/ltiadvantage/registration_view',
|
||||
$tcontext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a warning, indicating to the user that cookies are require but couldn't be set.
|
||||
*
|
||||
* @return string the html.
|
||||
*/
|
||||
public function render_cookies_required_notice(): string {
|
||||
$notification = new notification(get_string('cookiesarerequiredinfo', 'enrol_lti'), notification::NOTIFY_WARNING, false);
|
||||
$tcontext = [
|
||||
'heading' => get_string('cookiesarerequired', 'enrol_lti'),
|
||||
'notification' => $notification->export_for_template($this),
|
||||
];
|
||||
|
||||
return parent::render_from_template('enrol_lti/local/ltiadvantage/cookies_required_notice', $tcontext);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,7 @@ require_once(__DIR__.'/lib.php');
|
||||
global $CFG, $DB, $PAGE, $USER;
|
||||
require_once($CFG->libdir . '/filelib.php');
|
||||
require_login(null, false);
|
||||
|
||||
confirm_sesskey();
|
||||
require_sesskey();
|
||||
$launchid = required_param('launchid', PARAM_TEXT);
|
||||
$modules = optional_param_array('modules', [], PARAM_INT);
|
||||
$grades = optional_param_array('grades', [], PARAM_INT);
|
||||
|
||||
@@ -31,6 +31,10 @@ $string['addtocourse'] = 'Add to course';
|
||||
$string['addtogradebook'] = 'Add to gradebook';
|
||||
$string['allowframeembedding'] = 'Note: It is recommended that the site administration setting \'Allow frame embedding\' is enabled, so that tools are displayed within a frame rather than in a new window.';
|
||||
$string['authltimustbeenabled'] = 'Note: This plugin requires the LTI authentication plugin to be enabled too.';
|
||||
$string['cookiesarerequired'] = 'Cookies are blocked by your browser';
|
||||
$string['cookiesarerequiredinfo'] = 'This tool can\'t be launched because your browser seems to be blocking third-party cookies.
|
||||
<br><br>
|
||||
To use this tool, try changing your browser cookie settings or using a different browser.';
|
||||
$string['copiedtoclipboard'] = '{$a} copied to clipboard';
|
||||
$string['copytoclipboard'] = 'Copy to clipboard';
|
||||
$string['couldnotestablishproxy'] = 'Could not establish proxy with consumer.';
|
||||
|
||||
+26
-1
@@ -26,6 +26,7 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
use auth_lti\local\ltiadvantage\utility\cookie_helper;
|
||||
use enrol_lti\local\ltiadvantage\lib\issuer_database;
|
||||
use enrol_lti\local\ltiadvantage\lib\launch_cache_session;
|
||||
use enrol_lti\local\ltiadvantage\repository\application_registration_repository;
|
||||
@@ -39,7 +40,7 @@ require_once(__DIR__."/../../config.php");
|
||||
// See http://www.imsglobal.org/spec/security/v1p0/#step-1-third-party-initiated-login.
|
||||
// Validate these here, despite further validation in the LTI 1.3 library.
|
||||
$iss = required_param('iss', PARAM_URL); // Issuer URI of the calling platform.
|
||||
$loginhint = required_param('login_hint', PARAM_INT); // Platform ID for the person to login.
|
||||
$loginhint = required_param('login_hint', PARAM_RAW); // Platform ID for the person to login.
|
||||
$targetlinkuri = required_param('target_link_uri', PARAM_URL); // The took launch URL.
|
||||
|
||||
// Optional lti_message_hint. See https://www.imsglobal.org/spec/lti/v1p3#additional-login-parameters-0.
|
||||
@@ -76,6 +77,30 @@ if (empty($_REQUEST['client_id']) && !empty($_REQUEST['id'])) {
|
||||
$_REQUEST['client_id'] = $_REQUEST['id'];
|
||||
}
|
||||
|
||||
// Before beginning the OIDC authentication, ensure the MoodleSession cookie can be used. Browser-specific steps may need to be
|
||||
// taken to set cookies in 3rd party contexts. Skip the check if the user is already auth'd. This means that either cookies aren't
|
||||
// an issue in the current browser/launch context.
|
||||
if (!isloggedin()) {
|
||||
cookie_helper::do_cookie_check(new moodle_url('/enrol/lti/login.php', [
|
||||
'iss' => $iss,
|
||||
'login_hint' => $loginhint,
|
||||
'target_link_uri' => $targetlinkuri,
|
||||
'lti_message_hint' => $ltimessagehint,
|
||||
'client_id' => $_REQUEST['client_id'],
|
||||
]));
|
||||
if (!cookie_helper::cookies_supported()) {
|
||||
global $OUTPUT, $PAGE;
|
||||
$PAGE->set_context(context_system::instance());
|
||||
$PAGE->set_url(new moodle_url('/enrol/lti/login.php'));
|
||||
$PAGE->set_pagelayout('popup');
|
||||
echo $OUTPUT->header();
|
||||
$renderer = $PAGE->get_renderer('enrol_lti');
|
||||
echo $renderer->render_cookies_required_notice();
|
||||
echo $OUTPUT->footer();
|
||||
die();
|
||||
}
|
||||
}
|
||||
|
||||
// Now, do the OIDC login.
|
||||
LtiOidcLogin::new(
|
||||
new issuer_database(new application_registration_repository(), new deployment_repository()),
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
{{!
|
||||
This file is part of Moodle - http://moodle.org/
|
||||
|
||||
Moodle is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Moodle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template enrol_lti/local/ltiadvantage/cookies_required_notice
|
||||
|
||||
Displays a notice, reporting that cookies are required but couldn't be set.
|
||||
|
||||
Classes required for JS:
|
||||
* none
|
||||
|
||||
Data attributes required for JS:
|
||||
* none
|
||||
|
||||
Context variables required for this template:
|
||||
* heading
|
||||
* notification
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"heading": "Cookies are required",
|
||||
"notification": {
|
||||
"message": "You appear to be using an unsupported browser...",
|
||||
"extraclasses": "",
|
||||
"announce": true,
|
||||
"closebutton": false,
|
||||
"issuccess": false,
|
||||
"isinfo": false,
|
||||
"iswarning": true,
|
||||
"iserror": false
|
||||
}
|
||||
}
|
||||
}}
|
||||
<h3>{{heading}}</h3>
|
||||
{{#notification}}
|
||||
{{> core/notification}}
|
||||
{{/notification}}
|
||||
@@ -327,8 +327,9 @@ class externallib_test extends externallib_advanced_testcase {
|
||||
$this->expectExceptionMessage($exception['message']);
|
||||
} else {
|
||||
// Failed, only canview and exception are supported.
|
||||
$this->markTestIncomplete('Incomplete, only canview and exception are supported');
|
||||
throw new \coding_exception('Incomplete, only canview and exception are supported');
|
||||
}
|
||||
|
||||
// Switch to the user and assign the role.
|
||||
$this->setUser(${$user});
|
||||
role_assign($roleid, $USER->id, $coursecontext);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* stylelint-disable unit-blacklist */
|
||||
/* stylelint-disable unit-disallowed-list */
|
||||
/**************************************
|
||||
|
||||
Structure of the other user role assignment panel
|
||||
|
||||
@@ -138,9 +138,13 @@ class filter_mathjaxloader extends moodle_text_filter {
|
||||
|
||||
$hasdisplayorinline = false;
|
||||
if ($hasextra) {
|
||||
// Convert the HTML tag wrapper inside the equation to entities.
|
||||
$text = $this->escape_html_tag_wrapper($text);
|
||||
// If custom dilimeters are used, wrap whole text to prevent autolinking.
|
||||
$text = '<span class="nolink">' . $text . '</span>';
|
||||
} else if (preg_match('/\\\\[[(]/', $text) || preg_match('/\$\$/', $text)) {
|
||||
// Convert the HTML tag wrapper inside the equation to entities.
|
||||
$text = $this->escape_html_tag_wrapper($text);
|
||||
// Only parse the text if there are mathjax symbols in it. The recognized
|
||||
// math environments are \[ \] and $$ $$ for display mathematics and \( \)
|
||||
// for inline mathematics.
|
||||
@@ -246,4 +250,20 @@ class filter_mathjaxloader extends moodle_text_filter {
|
||||
$start,
|
||||
$end - $start + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes HTML tags within a string.
|
||||
*
|
||||
* This function replaces HTML tags enclosed in curly brackets with their respective HTML entities.
|
||||
*
|
||||
* @param string $text The input string containing HTML tags.
|
||||
* @return string Returns the input string with HTML tags escaped.
|
||||
*/
|
||||
private function escape_html_tag_wrapper(string $text): string {
|
||||
return preg_replace_callback('/\{([^}]+)\}/', function(array $matches): string {
|
||||
$search = ['<', '>'];
|
||||
$replace = ['<', '>'];
|
||||
return str_replace($search, $replace, $matches[0]);
|
||||
}, $text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
@gradingform @gradingform_guide @javascript
|
||||
Feature: Display marking guide information to students
|
||||
In order for students to see the marking guide information
|
||||
As a teacher
|
||||
I should be able to change display settings for marking guide information
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
| student1 | Student | 1 | student1@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
And the following "activities" exist:
|
||||
| activity | course | name | advancedgradingmethod_submissions |
|
||||
| assign | C1 | Assign 1 | guide |
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
And I go to "Assign 1" advanced grading definition page
|
||||
And I set the following fields to these values:
|
||||
| Name | Assign 1 marking guide |
|
||||
| Description | Marking guide description |
|
||||
And I define the following marking guide:
|
||||
| Criterion name | Description for students | Description for markers | Maximum score |
|
||||
| Grade Criteria 1 | Grade 1 description for students | Grade 1 description for markers | 70 |
|
||||
| Grade Criteria 2 | Grade 2 description for students | Grade 2 description for markers | 30 |
|
||||
And I press "Save marking guide and make it ready"
|
||||
|
||||
Scenario: Confirm that marking guide information is not displayed after student is graded
|
||||
# Update the existing marking guide to ensure that marks per criterion is displayed.
|
||||
Given I click on "Edit the current form definition" "link"
|
||||
And I set the field "Show marks per criterion to students" to "0"
|
||||
And I press "Save"
|
||||
And I am on the "Assign 1" "assign activity" page
|
||||
And I go to "Student 1" "Assign 1" activity advanced grading page
|
||||
And I grade by filling the marking guide with:
|
||||
| Grade Criteria 1 | 50 | Excellent work! |
|
||||
| Grade Criteria 2 | 20 | Try harder |
|
||||
And I press "Save changes"
|
||||
When I am on the "Assign 1" "assign activity" page logged in as student1
|
||||
# Confirm the marking guide information display after student is graded when marking per criterion display is disabled.
|
||||
# Confirm that overall grade is displayed.
|
||||
Then I should see "70.00 / 100.00"
|
||||
And I should see the marking guide information displayed as:
|
||||
| criteria | description | remark |
|
||||
| Grade Criteria 1 | Grade 1 description for students | Excellent work! |
|
||||
| Grade Criteria 2 | Grade 2 description for students | Try harder |
|
||||
|
||||
Scenario: Confirm that marking guide information is displayed after student is graded
|
||||
Given I am on the "Assign 1" "assign activity" page logged in as student1
|
||||
And I should see "Grade 1 description for students" in the "Grade Criteria 1" "table_row"
|
||||
And I should see "Grade 2 description for students" in the "Grade Criteria 2" "table_row"
|
||||
# No grade to student1 yet.
|
||||
And I should not see "70.00 / 100.00"
|
||||
# No need to update marking guide as marking guide definition is already enabled by default
|
||||
And I am on the "Assign 1" "assign activity" page logged in as teacher1
|
||||
And I go to "Student 1" "Assign 1" activity advanced grading page
|
||||
And I grade by filling the marking guide with:
|
||||
| Grade Criteria 1 | 50 | Excellent work! |
|
||||
| Grade Criteria 2 | 20 | Try harder |
|
||||
And I press "Save changes"
|
||||
When I am on the "Assign 1" "assign activity" page logged in as student1
|
||||
# Student1 grade is now displayed.
|
||||
Then I should see "70.00 / 100.00"
|
||||
And I should see the marking guide information displayed as:
|
||||
| criteria | description | remark | maxscore | criteriascore |
|
||||
| Grade Criteria 1 | Grade 1 description for students | Excellent work! | 70 | 50 / 70 |
|
||||
| Grade Criteria 2 | Grade 2 description for students | Try harder | 30 | 20 / 30 |
|
||||
@@ -0,0 +1,74 @@
|
||||
@gradingform @gradingform_guide
|
||||
Feature: Teacher can define a marking guide
|
||||
As a teacher,
|
||||
I should be able to define a marking guide
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firtname | lastname | email |
|
||||
| teacher1 | Teacher | One | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "activities" exist:
|
||||
| activity | course | name | advancedgradingmethod_submissions |
|
||||
| assign | C1 | Assign 1 | guide |
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
And I go to "Assign 1" advanced grading definition page
|
||||
And I set the following fields to these values:
|
||||
| Name | Marking guide 1 |
|
||||
|
||||
Scenario: No criterion added to marking guide
|
||||
When I press "Save as draft"
|
||||
# Confirm that criterion parameters are required
|
||||
Then I should see "Criterion name can not be empty"
|
||||
And I should see "Criterion max score can not be empty"
|
||||
# Confirm that marking guide is not saved due to the missing criterion
|
||||
And I should not see "Marking guide 1 Draft"
|
||||
And I should not see "Please note: the advanced grading form is not ready at the moment. Simple grading method will be used until the form has a valid status."
|
||||
|
||||
@javascript
|
||||
Scenario: Marking guide criterion is added to marking guide
|
||||
Given I define the following marking guide:
|
||||
| Criterion name | Description for students | Description for markers | Maximum score |
|
||||
| Criteria 1 | Criteria 1 description for student | Criteria 1 description for marker | 70 |
|
||||
| Criteria 2 | Criteria 2 description for student | Criteria 2 description for marker | 30 |
|
||||
# Move Criteria 1 below Criteria 2
|
||||
And I click on "Move down" "button" in the "Criteria 1" "table_row"
|
||||
When I press "Save as draft"
|
||||
And I go to "Assign 1" advanced grading definition page
|
||||
# Confirm that the order of criterion shown matches input -- Criteria 2 is listed before Criteria 1
|
||||
Then "Move down" "button" in the "Criteria 2" "table_row" should be visible
|
||||
And "Move up" "button" in the "Criteria 2" "table_row" should not be visible
|
||||
And "Move up" "button" in the "Criteria 1" "table_row" should be visible
|
||||
And "Move down" "button" in the "Criteria 1" "table_row" should not be visible
|
||||
# Confirm the other information entered were saved
|
||||
And I should see "Criteria 2 description for student" in the "Criteria 2" "table_row"
|
||||
And I should see "Criteria 2 description for marker" in the "Criteria 2" "table_row"
|
||||
And I should see "30" in the "Criteria 2" "table_row"
|
||||
And I should see "Criteria 1 description for student" in the "Criteria 1" "table_row"
|
||||
And I should see "Criteria 1 description for marker" in the "Criteria 1" "table_row"
|
||||
And I should see "70" in the "Criteria 1" "table_row"
|
||||
|
||||
Scenario: Marking guide options and frequently used comment are added to marking guide
|
||||
Given I define the following marking guide:
|
||||
| Criterion name | Description for students | Description for markers | Maximum score |
|
||||
| Criteria 1 | Criteria 1 description for student | Criteria 1 description for marker | 50 |
|
||||
| Criteria 2 | Criteria 2 description for student | Criteria 2 description for marker | 50 |
|
||||
# Add frequently used comments and other marking guide options
|
||||
And I define the following frequently used comments:
|
||||
| Comment 1 |
|
||||
| Comment 2 |
|
||||
And I set the following fields to these values:
|
||||
| Show guide definition to students | 1 |
|
||||
| Show marks per criterion to students | 0 |
|
||||
When I press "Save as draft"
|
||||
And I go to "Assign 1" advanced grading definition page
|
||||
# Confirm that frequently used comments and marking guide options specified during registration are retained
|
||||
Then I should see "Comment 1"
|
||||
And I should see "Comment 2"
|
||||
And the field "Show guide definition to students" matches value "1"
|
||||
And the field "Show marks per criterion to students" matches value "0"
|
||||
@@ -187,14 +187,9 @@ class user extends tablelike implements selectable_items {
|
||||
$itemicon = html_writer::div($this->format_icon($item), 'mr-1');
|
||||
$itemtype = \html_writer::span($this->structure->get_element_type_string($gradetreeitem),
|
||||
'd-block text-uppercase small dimmed_text');
|
||||
// If a behat test site is running avoid outputting the information about the type of the grade item.
|
||||
// This additional information currently causes issues in behat particularly with the existing xpath used to
|
||||
// interact with table elements.
|
||||
if (!defined('BEHAT_SITE_RUNNING')) {
|
||||
$itemcontent = html_writer::div($itemtype . $itemname);
|
||||
} else {
|
||||
$itemcontent = html_writer::div($itemname);
|
||||
}
|
||||
|
||||
$itemtitle = html_writer::div($itemname, 'rowtitle');
|
||||
$itemcontent = html_writer::div($itemtype . $itemtitle);
|
||||
|
||||
$line = [
|
||||
html_writer::div($itemicon . $itemcontent . $lockicon, "{$type} d-flex align-items-center"),
|
||||
|
||||
@@ -556,14 +556,8 @@ class user extends grade_report {
|
||||
}
|
||||
|
||||
// Generate the content for a cell that represents a grade item.
|
||||
// If a behat test site is running avoid outputting the information about the type of the grade item.
|
||||
// This additional information causes issues in behat particularly with the existing xpath used to
|
||||
// interact with table elements.
|
||||
if (!defined('BEHAT_SITE_RUNNING')) {
|
||||
$content = \html_writer::div($itemtype . $fullname);
|
||||
} else {
|
||||
$content = \html_writer::div($fullname);
|
||||
}
|
||||
$itemtitle = \html_writer::div($fullname, 'rowtitle');
|
||||
$content = \html_writer::div($itemtype . $itemtitle);
|
||||
|
||||
// Name.
|
||||
$data['itemname']['content'] = \html_writer::div($itemicon . $content, "{$type} d-flex align-items-center");
|
||||
|
||||
@@ -79,6 +79,7 @@ class user_groups_editable extends \core\output\inplace_editable {
|
||||
}
|
||||
|
||||
$fullname = fullname($user, has_capability('moodle/site:viewfullnames', $this->context));
|
||||
$fullname = htmlspecialchars($fullname, ENT_QUOTES, 'utf-8');
|
||||
$this->edithint = get_string('editusersgroupsa', 'group', $fullname);
|
||||
$this->editlabel = get_string('editusersgroupsa', 'group', $fullname);
|
||||
|
||||
|
||||
+1
-4
@@ -478,7 +478,7 @@ if ($config->stage == INSTALL_DATABASE) {
|
||||
if ($hint_database !== '') {
|
||||
echo '<div class="alert alert-danger">'.$hint_database.'</div>';
|
||||
}
|
||||
echo '</div>';
|
||||
|
||||
install_print_footer($config);
|
||||
die;
|
||||
}
|
||||
@@ -610,11 +610,8 @@ if ($config->stage == INSTALL_PATHS) {
|
||||
if ($hint_admindir !== '') {
|
||||
echo '<div class="alert alert-danger">'.$hint_admindir.'</div>';
|
||||
}
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
|
||||
install_print_footer($config);
|
||||
die;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,17 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['cannotcreatedboninstall'] = '<p>Не може да се създаде базата данни.</p>
|
||||
<p>Посочената база данни не съществува и даденият потребител няма разрешение да създаде базата данни.</p>
|
||||
<p>Администраторът на сайта трябва да провери конфигурацията на базата данни.</p>';
|
||||
$string['cannotcreatelangdir'] = 'Не може да се създаде езикова директория';
|
||||
$string['cannotcreatetempdir'] = 'Не може да създаде временна директория';
|
||||
$string['cannotdownloadcomponents'] = 'Не могат да се изтеглят компоненти';
|
||||
$string['cannotdownloadzipfile'] = 'Не може да се изтегли ZIP файл';
|
||||
$string['cannotfindcomponent'] = 'Не можа да намери компонент';
|
||||
$string['remotedownloaderror'] = 'Изтеглянето на компонента към вашия сървър пропадна, проверете настройките на proxy, препоръчително е PHP разширението cURL.<br /><br />Вие трябва ръчно да изтеглите файла <a href="{$a->url}">{$a->url}</a>, да го копирате в директория {$a->dest} на вашия сървър и да го разархивирате там.';
|
||||
$string['cannotunzipfile'] = 'Файлът не може да се разархивира';
|
||||
$string['componentisuptodate'] = 'Компонентът е актуален';
|
||||
$string['remotedownloaderror'] = '<p>Изтеглянето на компонента към вашия сървър пропадна, проверете настройките на proxy; препоръчително е PHP разширението cURL.</p><p>Вие трябва ръчно да изтеглите файла <a href="{$a->url}">{$a->url}</a>, да го копирате в директория "{$a->dest}" на вашия сървър и да го разархивирате там.</p>';
|
||||
$string['wrongdestpath'] = 'Грешен път към целта';
|
||||
$string['wrongsourcebase'] = 'Грешен изходен адрес';
|
||||
$string['wrongzipfilename'] = 'Грешно име на ZIP файл-а';
|
||||
|
||||
@@ -33,3 +33,9 @@ $string['clianswerno'] = 'n';
|
||||
$string['cliansweryes'] = 's';
|
||||
$string['cliincorrectvalueerror'] = 'Error, valor incorrecto "{$a->value}" para "{$a->option}"';
|
||||
$string['cliincorrectvalueretry'] = 'Valor incorrecto, por favor reintentar';
|
||||
$string['clitypevalue'] = 'tipear el valor';
|
||||
$string['clitypevaluedefault'] = 'tipear el valor, presionar Enter para usar el valor por defecto ({$a})';
|
||||
$string['cliunknowoption'] = 'Opciones no reconocidas:
|
||||
{$a}
|
||||
Utilice la opción --help.';
|
||||
$string['cliyesnoprompt'] = 'Tipear y (significa sí) o n (significa no)';
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Automatically generated strings for Moodle installer
|
||||
*
|
||||
* Do not edit this file manually! It contains just a subset of strings
|
||||
* needed during the very first steps of installation. This file was
|
||||
* generated automatically by export-installer.php (which is part of AMOS
|
||||
* {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
|
||||
* list of strings defined in /install/stringnames.txt.
|
||||
*
|
||||
* @package installer
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['language'] = 'Idioma';
|
||||
$string['moodlelogo'] = 'Logo de Moodle';
|
||||
$string['next'] = 'Siguiente';
|
||||
$string['previous'] = 'Previo';
|
||||
@@ -37,8 +37,8 @@ $string['clitypevalue'] = 'type valeur';
|
||||
$string['clitypevaluedefault'] = 'type valeur, tapez Entrée pour utiliser la valeur par défaut ({$a})';
|
||||
$string['cliunknowoption'] = 'Options non reconnues :
|
||||
{$a}.
|
||||
Utilisez l\'option --help.';
|
||||
Utilisez l’option --help.';
|
||||
$string['cliyesnoprompt'] = 'Tapez y (pour oui) ou n (pour non)';
|
||||
$string['environmentrequireinstall'] = 'L\'extension doit être installée et activée';
|
||||
$string['environmentrequireinstall'] = 'L’extension doit être installée et activée';
|
||||
$string['environmentrequireversion'] = 'La version {$a->needed} est requise ; vous utilisez actuellement la version {$a->current}';
|
||||
$string['upgradekeyset'] = 'Mettre à jour la clef (laisser vide pour ne pas la définir)';
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['cannotcreatedboninstall'] = '<p>Impossible de créer la base de données.</p>
|
||||
<p>La base de données indiquées n\'existe pas et l\'utilisateur spécifié n\'a pas les permissions requises pour créer une base de données.</p>.
|
||||
<p>L\'administrateur du site doit revoir la configuration de la base de données.</p>';
|
||||
<p>La base de données indiquées n’existe pas et l’utilisateur spécifié n’a pas les permissions requises pour créer une base de données.</p>.
|
||||
<p>L’administrateur du site doit revoir la configuration de la base de données.</p>';
|
||||
$string['cannotcreatelangdir'] = 'Création du dossier lang impossible';
|
||||
$string['cannotcreatetempdir'] = 'Création du dossier temp impossible';
|
||||
$string['cannotdownloadcomponents'] = 'Téléchargement des composants impossible';
|
||||
@@ -43,10 +43,10 @@ $string['cannotunzipfile'] = 'Décompression du fichier ZIP impossible';
|
||||
$string['componentisuptodate'] = 'Le composant est à jour';
|
||||
$string['dmlexceptiononinstall'] = '<p>Une erreur de base de données est survenue [{$a->errorcode}].<br />{$a->debuginfo}</p>';
|
||||
$string['downloadedfilecheckfailed'] = 'La vérification du fichier téléchargé à échoué';
|
||||
$string['invalidmd5'] = 'Le code de contrôle md5 n\'est pas valide';
|
||||
$string['missingrequiredfield'] = 'Un champ obligatoire n\'est pas renseigné';
|
||||
$string['remotedownloaderror'] = '<p>Le téléchargement du composant sur votre serveur a échoué. Veuillez vérifier les réglages de proxy. L\'extension cURL de PHP est vivement recommandée.</p>
|
||||
<p>Vous devez télécharger manuellement le fichier <a href="{$a->url}">{$a->url}</a>, le copier sur votre serveur à l\'emplacement « {$a->dest} » et le décompresser à cet endroit.</p>';
|
||||
$string['invalidmd5'] = 'Le code de contrôle md5 n’est pas valide';
|
||||
$string['missingrequiredfield'] = 'Un champ obligatoire n’est pas renseigné';
|
||||
$string['remotedownloaderror'] = '<p>Le téléchargement du composant sur votre serveur a échoué. Veuillez vérifier les réglages de proxy. L’extension cURL de PHP est vivement recommandée.</p>
|
||||
<p>Vous devez télécharger manuellement le fichier <a href="{$a->url}">{$a->url}</a>, le copier sur votre serveur à l’emplacement « {$a->dest} » et le décompresser à cet endroit.</p>';
|
||||
$string['wrongdestpath'] = 'Chemin de destination incorrect';
|
||||
$string['wrongsourcebase'] = 'Adresse URL de base de la source incorrecte';
|
||||
$string['wrongzipfilename'] = 'Nom de fichier ZIP incorrect';
|
||||
|
||||
+29
-29
@@ -32,50 +32,50 @@ defined('MOODLE_INTERNAL') || die();
|
||||
$string['admindirname'] = 'Dossier administration';
|
||||
$string['availablelangs'] = 'Paquetages de langue disponibles';
|
||||
$string['chooselanguagehead'] = 'Choisissez une langue';
|
||||
$string['chooselanguagesub'] = 'Veuillez choisir la langue d\'installation. Cette langue sera utilisée comme langue par défaut du site, que vous pourrez modifier ultérieurement.';
|
||||
$string['chooselanguagesub'] = 'Veuillez choisir la langue d’installation. Cette langue sera utilisée comme langue par défaut du site, que vous pourrez modifier ultérieurement.';
|
||||
$string['clialreadyconfigured'] = 'Le fichier config.php existe déjà. Veuillez utiliser admin/cli/install_database.php pour installer Moodle sur ce site.';
|
||||
$string['clialreadyinstalled'] = 'Le fichier config.php existe déjà. Veuillez utiliser admin/cli/install_database.php si vous désirez mettre à jour ce site Moodle.';
|
||||
$string['cliinstallheader'] = 'Programme d\'installation de Moodle {$a} en ligne de commande';
|
||||
$string['clitablesexist'] = 'Les tables de la base de données sont déjà présentes ; l\'installation en ligne de commande ne peut pas continuer.';
|
||||
$string['cliinstallheader'] = 'Programme d’installation de Moodle {$a} en ligne de commande';
|
||||
$string['clitablesexist'] = 'Les tables de la base de données sont déjà présentes ; l’installation en ligne de commande ne peut pas continuer.';
|
||||
$string['databasehost'] = 'Serveur de base de données';
|
||||
$string['databasename'] = 'Nom de la base de données';
|
||||
$string['databasetypehead'] = 'Sélectionner un pilote de base de données';
|
||||
$string['dataroot'] = 'Dossier de données';
|
||||
$string['datarootpermission'] = 'Droits d\'accès au dossier de données';
|
||||
$string['datarootpermission'] = 'Droits d’accès au dossier de données';
|
||||
$string['dbprefix'] = 'Préfixe des tables';
|
||||
$string['dirroot'] = 'Dossier Moodle';
|
||||
$string['environmenthead'] = 'Vérification de l\'environnement…';
|
||||
$string['environmentsub2'] = 'Chaque version de Moodle nécessite une version minimale de certains composants PHP et des extensions de PHP obligatoires. Une vérification complète de l\'environnement est effectuée avec chaque installation et chaque mise à jour. Veuillez contacter l\'administrateur du serveur si vous ne savez pas comment installer une nouvelle version ou activer des extensions de PHP.';
|
||||
$string['errorsinenvironment'] = 'Échec de la vérification de l\'environnement !';
|
||||
$string['environmenthead'] = 'Vérification de l’environnement…';
|
||||
$string['environmentsub2'] = 'Chaque version de Moodle nécessite une version minimale de certains composants PHP et des extensions de PHP obligatoires. Une vérification complète de l’environnement est effectuée avec chaque installation et chaque mise à jour. Veuillez contacter l’administrateur du serveur si vous ne savez pas comment installer une nouvelle version ou activer des extensions de PHP.';
|
||||
$string['errorsinenvironment'] = 'Échec de la vérification de l’environnement !';
|
||||
$string['installation'] = 'Installation';
|
||||
$string['langdownloaderror'] = 'La langue « {$a} » n\'a pas pu être téléchargée. La suite de l\'installation se déroulera en anglais. Vous pourrez télécharger et installer d\'autres langues à la fin de l\'installation';
|
||||
$string['langdownloaderror'] = 'La langue « {$a} » n’a pas pu être téléchargée. La suite de l’installation se déroulera en anglais. Vous pourrez télécharger et installer d’autres langues à la fin de l’installation';
|
||||
$string['memorylimithelp'] = '<p>La limite de mémoire de PHP sur votre serveur est actuellement de {$a}.</p>
|
||||
<p>Cette valeur trop basse risque de générer des problèmes de manque de mémoire pour Moodle, notamment si vous utilisez beaucoup de modules et/ou si vous avez un grand nombre d\'utilisateurs.</p>
|
||||
<p>Cette valeur trop basse risque de générer des problèmes de manque de mémoire pour Moodle, notamment si vous utilisez beaucoup de modules et/ou si vous avez un grand nombre d’utilisateurs.</p>
|
||||
<p>Il est recommandé de configurer PHP avec une limite de mémoire aussi élevée que possible, par exemple 40 Mo. Vous pouvez obtenir cela de différentes façons :</p>
|
||||
<ol>
|
||||
<li>si vous en avez la possibilité, recompilez PHP avec l\'option <em>--enable-memory-limit</em>. Cela permettra à Moodle de fixer lui-même sa limite de mémoire ;</li>
|
||||
<li>si vous avez accès à votre fichier « php.ini », vous pouvez attribuer au paramètre <b>memory_limit</b> une valeur comme 40M. Si vous n\'y avez pas accès, demandez à l\'administrateur de le faire pour vous ;</li>
|
||||
<li>si vous en avez la possibilité, recompilez PHP avec l’option <em>--enable-memory-limit</em>. Cela permettra à Moodle de fixer lui-même sa limite de mémoire ;</li>
|
||||
<li>si vous avez accès à votre fichier « php.ini », vous pouvez attribuer au paramètre <b>memory_limit</b> une valeur comme 40M. Si vous n’y avez pas accès, demandez à l’administrateur de le faire pour vous ;</li>
|
||||
<li>sur certains serveurs, vous pouvez créer dans le dossier principal de Moodle un fichier « .htaccess » contenant cette ligne :
|
||||
<blockquote><div>php_value memory_limit 40M</div></blockquote>
|
||||
<p>Cependant, sur certains serveurs, cela empêchera le fonctionnement correct de <b>tous</b> les fichiers PHP (vous verrez s\'afficher des erreurs lors de la consultation de pages). Dans ce cas, vous devrez supprimer le fichier « .htaccess ».</p></li>
|
||||
<p>Cependant, sur certains serveurs, cela empêchera le fonctionnement correct de <b>tous</b> les fichiers PHP (vous verrez s’afficher des erreurs lors de la consultation de pages). Dans ce cas, vous devrez supprimer le fichier « .htaccess ».</p></li>
|
||||
</ol>';
|
||||
$string['paths'] = 'Chemins';
|
||||
$string['pathserrcreatedataroot'] = 'Le dossier de données ({$a->dataroot}) ne peut pas être créé par l\'installeur.';
|
||||
$string['pathshead'] = 'Confirmer les chemins d\'accès';
|
||||
$string['pathsrodataroot'] = 'Le dossier de données n\'est pas atteignable en écriture.';
|
||||
$string['pathsroparentdataroot'] = 'Le dossier parent ({$a->parent}) n\'est pas atteignable en écriture. Le dossier de données ({$a->dataroot}) ne peut pas être créé par le programme d\'installation.';
|
||||
$string['pathssubadmindir'] = 'Quelques rares hébergeurs utilisent « /admin » comme URL spéciale pour l\'accès à un tableau de bord ou d\'autres fonctionnalités. Malheureusement ceci entre en conflit avec l\'emplacement standard des pages d\'administration de Moodle. On peut corriger ceci en renommant le dossier admin de l\'installation Moodle et en plaçant le nouveau nom choisi dans ce champ. Par exemple, <em>moodleadmin</em>. Ceci modifiera tous les liens de l\'administration de Moodle.';
|
||||
$string['pathserrcreatedataroot'] = 'Le dossier de données ({$a->dataroot}) ne peut pas être créé par l’installeur.';
|
||||
$string['pathshead'] = 'Confirmer les chemins d’accès';
|
||||
$string['pathsrodataroot'] = 'Le dossier de données n’est pas atteignable en écriture.';
|
||||
$string['pathsroparentdataroot'] = 'Le dossier parent ({$a->parent}) n’est pas atteignable en écriture. Le dossier de données ({$a->dataroot}) ne peut pas être créé par le programme d’installation.';
|
||||
$string['pathssubadmindir'] = 'Quelques rares hébergeurs utilisent « /admin » comme URL spéciale pour l’accès à un tableau de bord ou d’autres fonctionnalités. Malheureusement ceci entre en conflit avec l’emplacement standard des pages d’administration de Moodle. On peut corriger ceci en renommant le dossier admin de l’installation Moodle et en plaçant le nouveau nom choisi dans ce champ. Par exemple, <em>moodleadmin</em>. Ceci modifiera tous les liens de l’administration de Moodle.';
|
||||
$string['pathssubdataroot'] = '<p>Un dossier dans lequel Moodle stockera tous les fichiers qui seront déposés par les utilisateurs.</p>
|
||||
<p>Ce dossier doit être atteignable en lecture et en écriture par l\'utilisateur système du serveur web (habituellement nommé « www-data », « nobody » ou encore « apache »).</p>
|
||||
<p>Ce dossier doit être atteignable en lecture et en écriture par l’utilisateur système du serveur web (habituellement nommé « www-data », « nobody » ou encore « apache »).</p>
|
||||
<p>Il ne doit pas pouvoir être atteint directement via le web.</p>
|
||||
<p>Si ce dossier n\'existe pas encore, Moodle tentera de le créer au cours du processus d\'installation.</p>';
|
||||
$string['pathssubdirroot'] = '<p>Le chemin d\'accès complet au dossier contenant le code source de Moodle.</p>';
|
||||
$string['pathssubwwwroot'] = '<p>L\'adresse web complète par laquelle on accédera à Moodle, i.e. l\'adresse que les utilisateurs saisiront dans la barre d\'adresse de leur navigateur pour accéder à Moodle.</p>
|
||||
<p>Il n\'est pas possible d\'accéder à Moodle depuis plusieurs adresses web différentes. Si votre site web peut être atteint au moyen de plusieurs adresses, saisissez ici la plus simple d\'entre elles et définissez des redirections permanentes pour toutes les autres adresses.</p>
|
||||
<p>Si votre site peut être atteint depuis Internet et depuis un réseau interne (un intranet), indiquez ici l\'adresse publique.</p>
|
||||
<p>Si l\'adresse indiquée actuellement n\'est pas correcte, veuillez modifier l\'URL dans la barre d\'adresse de votre navigateur et recommencer l\'installation.</p>';
|
||||
$string['pathsunsecuredataroot'] = 'L\'emplacement du dossier de données n\'est pas sûr';
|
||||
$string['pathswrongadmindir'] = 'Le dossier d\'administration n\'existe pas';
|
||||
<p>Si ce dossier n’existe pas encore, Moodle tentera de le créer au cours du processus d’installation.</p>';
|
||||
$string['pathssubdirroot'] = '<p>Le chemin d’accès complet au dossier contenant le code source de Moodle.</p>';
|
||||
$string['pathssubwwwroot'] = '<p>L’adresse web complète par laquelle on accédera à Moodle, i.e. l’adresse que les utilisateurs saisiront dans la barre d’adresse de leur navigateur pour accéder à Moodle.</p>
|
||||
<p>Il n’est pas possible d’accéder à Moodle depuis plusieurs adresses web différentes. Si votre site web peut être atteint au moyen de plusieurs adresses, saisissez ici la plus simple d’entre elles et définissez des redirections permanentes pour toutes les autres adresses.</p>
|
||||
<p>Si votre site peut être atteint depuis Internet et depuis un réseau interne (un intranet), indiquez ici l’adresse publique.</p>
|
||||
<p>Si l’adresse indiquée actuellement n’est pas correcte, veuillez modifier l’URL dans la barre d’adresse de votre navigateur et recommencer l’installation.</p>';
|
||||
$string['pathsunsecuredataroot'] = 'L’emplacement du dossier de données n’est pas sûr';
|
||||
$string['pathswrongadmindir'] = 'Le dossier d’administration n’existe pas';
|
||||
$string['phpextension'] = 'Extension PHP {$a}';
|
||||
$string['phpversion'] = 'Version de PHP';
|
||||
$string['phpversionhelp'] = '<p>Moodle nécessite au minimum la version 5.6.5 ou 7.1 (la version 7.0.x a des limitations avec certains moteurs de base de données).</p>
|
||||
@@ -85,7 +85,7 @@ $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
|
||||
$string['welcomep20'] = 'Vous voyez cette page, car vous avez installé Moodle correctement et lancé le logiciel <strong>{$a->packname} {$a->packversion}</strong> sur votre ordinateur. Félicitations !';
|
||||
$string['welcomep30'] = 'Cette version de <b>{$a->installername}</b> comprend des logiciels qui créent un environnement dans lequel <b>Moodle</b> va fonctionner, à savoir :';
|
||||
$string['welcomep40'] = 'Ce paquet contient également <b>Moodle {$a->moodlerelease} ({$a->moodleversion})</b>.';
|
||||
$string['welcomep50'] = 'L\'utilisation de tous les logiciels de ce paquetage est soumis à l\'acceptation de leurs licences respectives. Le paquetage complet <strong>{$a->installername}</strong> est un <a href="https://www.opensource.org/docs/definition_plain.html">logiciel libre</a>. Il est distribué sous licence <a href="https://www.gnu.org/copyleft/gpl.html">GPL</a>.';
|
||||
$string['welcomep60'] = 'Les pages suivantes vous aideront pas à pas à configurer et installer <strong>Moodle</strong> sur votre ordinateur. Il vous sera possible d\'accepter les réglages par défaut ou, facultativement, de les adapter à vos propres besoins.';
|
||||
$string['welcomep70'] = 'Cliquer sur le bouton « Suivant » ci-dessous pour continuer l\'installation de <b>Moodle</b>.';
|
||||
$string['welcomep50'] = 'L’utilisation de tous les logiciels de ce paquetage est soumis à l’acceptation de leurs licences respectives. Le paquetage complet <strong>{$a->installername}</strong> est un <a href="https://www.opensource.org/docs/definition_plain.html">logiciel libre</a>. Il est distribué sous licence <a href="https://www.gnu.org/copyleft/gpl.html">GPL</a>.';
|
||||
$string['welcomep60'] = 'Les pages suivantes vous aideront pas à pas à configurer et installer <strong>Moodle</strong> sur votre ordinateur. Il vous sera possible d’accepter les réglages par défaut ou, facultativement, de les adapter à vos propres besoins.';
|
||||
$string['welcomep70'] = 'Cliquer sur le bouton « Suivant » ci-dessous pour continuer l’installation de <b>Moodle</b>.';
|
||||
$string['wwwroot'] = 'Adresse web';
|
||||
|
||||
@@ -46,7 +46,7 @@ $string['dbprefix'] = 'Tabellprefix';
|
||||
$string['dirroot'] = 'Moodles filområde';
|
||||
$string['environmenthead'] = 'Sjekker "omgivelsene" dine...';
|
||||
$string['environmentsub2'] = 'Hver Moodle-utgivelse har minimumskrav til PHP versjon og et sett med obligatoriske PHP-tillegg. En full sjekk av omgivelser gjøres før hver installering og oppgradering. Vær vennlig å kontakte serveradministrator hvis du ikke vet hvordan du skal installere nye versjoner eller slå på PHP-tillegg.';
|
||||
$string['errorsinenvironment'] = 'Omgivelseskontroll feilet!';
|
||||
$string['errorsinenvironment'] = 'Miljøkontroll feilet!';
|
||||
$string['installation'] = 'Installasjon';
|
||||
$string['langdownloaderror'] = 'Beklageligvis kunne ikke språkpakken "{$a}" lastes ned. Installasjonsprosessen vil fortsette på Engelsk.';
|
||||
$string['memorylimithelp'] = '<p>PHP minnegrensen for serveren din er nå satt til {$a}.</p>
|
||||
|
||||
@@ -32,7 +32,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
$string['admindirname'] = 'Pasta de administração';
|
||||
$string['availablelangs'] = 'Pacotes linguísticos disponíveis';
|
||||
$string['chooselanguagehead'] = 'Selecione um idioma';
|
||||
$string['chooselanguagesub'] = 'Selecione o idioma a utilizar durante a instalação. O idioma escolhido será definido como o predefinido mas poderá depois selecionar outro(s) idioma(s) para o site e para os utilizadores.';
|
||||
$string['chooselanguagesub'] = 'Selecione o idioma a utilizar durante a instalação. O idioma escolhido será definido como o predefinido, mas poderá depois selecionar outro(s) idioma(s) para o site e para os utilizadores.';
|
||||
$string['clialreadyconfigured'] = 'O ficheiro config.php já existe. Use \'admin/cli/install_database.php\' para instalar o Moodle para este site.';
|
||||
$string['clialreadyinstalled'] = 'O ficheiro config.php já existe, use admin/cli/install_database.php para atualizar o Moodle para este site.';
|
||||
$string['cliinstallheader'] = 'Programa para instalação do Moodle <b>{$a}</b> através da linha de comandos';
|
||||
@@ -56,9 +56,9 @@ $string['pathshead'] = 'Confirmar caminhos';
|
||||
$string['pathsrodataroot'] = 'A pasta dos dados é só de leitura (não permite escrita).';
|
||||
$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. 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>Pasta onde o Moodle irá armazenar todo o conteúdo de ficheiros enviados pelos utilizadores.</p>
|
||||
<p>O utilizador do Moodle no servidor web (normalmente <b>nobody</b>, <b>apache</b> ou <b>www-data</b>) deve ter permissão de <b>leitura</b> e <b>escrita</b> nessa pasta.
|
||||
<p>Não deve ser acessível diretamente através da web.</p>
|
||||
$string['pathssubdataroot'] = '<p>Pasta onde o Moodle irá armazenar todo o conteúdo dos ficheiros enviados pelos utilizadores.</p>
|
||||
<p>O utilizador do Moodle no servidor web (normalmente <b>nobody</b>, <b>apache</b> ou <b>www-data</b>) deve ter permissão de <b>leitura</b> e de <b>escrita</b> nessa pasta.
|
||||
<p>Não pode ser acessível diretamente através da web.</p>
|
||||
<p>Se a pasta não existir, o processo de instalação tentará criá-la.</p>';
|
||||
$string['pathssubdirroot'] = 'Caminho completo para a pasta que contém o código Moodle.';
|
||||
$string['pathssubwwwroot'] = 'Endereço web completo de acesso ao Moodle. Não é possível aceder ao Moodle usando mais do que um endereço. Se o site tiver mais do que um endereço público, devem ser configurados redirecionamentos permanentes em todos eles, à exceção deste. Se o site pode ser acedido a partir da Internet e de Intranet, use o endereço público aqui. Se o endereço atual não está correto, altere o endereço indicado na barra de endereço do seu navegador e reinicie a instalação.';
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Automatically generated strings for Moodle installer
|
||||
*
|
||||
* Do not edit this file manually! It contains just a subset of strings
|
||||
* needed during the very first steps of installation. This file was
|
||||
* generated automatically by export-installer.php (which is part of AMOS
|
||||
* {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
|
||||
* list of strings defined in /install/stringnames.txt.
|
||||
*
|
||||
* @package installer
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['cliincorrectvalueretry'] = 'Noto\'g\'ri qiymat, iltimos qayta urinib ko\'ring';
|
||||
$string['clitypevalue'] = 'qiymatni kiritish';
|
||||
$string['clitypevaluedefault'] = 'qiymatni kiriting, ({$a}) odatiy qiymatni ishlatish uchun Enter tumgasini bosing';
|
||||
@@ -942,6 +942,7 @@ $string['opensslrecommended'] = 'Installing the optional OpenSSL library is high
|
||||
$string['opensslrequired'] = 'The OpenSSL PHP extension is now required by Moodle to provide stronger cryptographic services.';
|
||||
$string['opentowebcrawlers'] = 'Open to search engines';
|
||||
$string['optionalmaintenancemessage'] = 'Optional maintenance message';
|
||||
$string['oracledatabaseinuse'] = 'We are changing Oracle DB support in Moodle LMS. Moodle version 4.5 will be the last version that supports Oracle as a database architecture. Further information can be found <a href="https://moodle.org/mod/forum/discuss.php?d=456122" target="_blank">here</a>';
|
||||
$string['order1'] = 'First';
|
||||
$string['order2'] = 'Second';
|
||||
$string['order3'] = 'Third';
|
||||
@@ -1454,6 +1455,7 @@ $string['unsupportedphpversion74'] = 'PHP version 7.4 and higher are not support
|
||||
$string['unsupportedphpversion80'] = 'PHP version 8.0 and higher are not supported.';
|
||||
$string['unsupportedphpversion81'] = 'PHP version 8.1 and higher are not supported.';
|
||||
$string['unsupportedphpversion82'] = 'PHP version 8.2 and higher are not supported.';
|
||||
$string['unsupportedphpversion83'] = 'PHP version 8.3 and higher are not supported.';
|
||||
$string['unsuspenduser'] = 'Activate user account';
|
||||
$string['updateaccounts'] = 'Update existing accounts';
|
||||
$string['updatecomponent'] = 'Update component';
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -180,7 +180,6 @@ const attemptFocus = focusTarget => {
|
||||
focusTarget.focus();
|
||||
} catch (e) {
|
||||
// Ignore failures. We will just try to focus the next element in the list.
|
||||
// eslint-disable-line
|
||||
}
|
||||
|
||||
ignoreFocusChanges = false;
|
||||
|
||||
@@ -101,6 +101,7 @@ function($, log, autoScroll, str, ModalFactory, ModalEvents, Notification) {
|
||||
|
||||
try {
|
||||
options = Object.defineProperty({}, "passive", {
|
||||
// eslint-disable-next-line getter-return
|
||||
get: function() {
|
||||
passivesupported = true;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user