Compare commits
424 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 143c2cd5be | |||
| 06b6467f4f | |||
| 452279abad | |||
| a51e542e5b | |||
| 76c0c9ed59 | |||
| 4443ecf6b4 | |||
| faadca21a9 | |||
| bcb96be529 | |||
| 8fdda09338 | |||
| 593204b45d | |||
| c988992f7e | |||
| 4408c3fcaa | |||
| 6c08d1c101 | |||
| dd433ba094 | |||
| fcd361badb | |||
| bef5f4a121 | |||
| f9e6040e28 | |||
| d77f2d55ef | |||
| de7ec71ca4 | |||
| de366cea22 | |||
| 6bc016ad41 | |||
| df23edbbab | |||
| 33b34bc86a | |||
| ffc51ab3fd | |||
| d706265b8e | |||
| 15912783c4 | |||
| b9dd2d29c5 | |||
| 99748cdee6 | |||
| 692cda1525 | |||
| 951a3d5e5c | |||
| 07dab571dd | |||
| 4544ea122b | |||
| d16fc3c7e3 | |||
| 007e527c3c | |||
| fea05fb8a3 | |||
| 84ef1906e0 | |||
| 744065174c | |||
| 1e2ef693f7 | |||
| 182e09ab5c | |||
| ca0e61a79d | |||
| f0aa5e9668 | |||
| 814fea407a | |||
| 1ae95e3b63 | |||
| f99f693238 | |||
| ae48b9e8a6 | |||
| f28b4d1ae1 | |||
| ca3218b23f | |||
| 8a1547bb17 | |||
| 1872094df6 | |||
| 34d5c59153 | |||
| 8aed364c22 | |||
| bdd0d80a09 | |||
| cbd81332e3 | |||
| 33fd0471e8 | |||
| b81756cc04 | |||
| 98a9e324d8 | |||
| a341231108 | |||
| c5c52e66f3 | |||
| 6e5d277fc2 | |||
| b6394a0b60 | |||
| 6edfe1b415 | |||
| 95c957791e | |||
| 217154895c | |||
| 076f6e9464 | |||
| 24c4875f6a | |||
| 78cac12d8e | |||
| f990fcdca7 | |||
| 59cf12eca4 | |||
| 3f01eeb49f | |||
| ffa09ce293 | |||
| 5f8bfd504a | |||
| df99388d27 | |||
| 811b9e4187 | |||
| 4971008242 | |||
| 510f9e3c08 | |||
| 0cd42e33bf | |||
| a0cf83c8f5 | |||
| cec3a6438b | |||
| 220637457e | |||
| 725f6f35a3 | |||
| 441ab036e0 | |||
| 596b317c27 | |||
| c9fe574fdd | |||
| e5f200616e | |||
| 61282aa692 | |||
| f67fa427bd | |||
| ff4d5f3caa | |||
| 829a0d615a | |||
| 9c5db33257 | |||
| 2f5def5d83 | |||
| 2f59f5b835 | |||
| fecf2ee25a | |||
| bf9e1f978e | |||
| 01a0c19364 | |||
| 0b8ce298cb | |||
| e2b856751a | |||
| 94eb59c5b8 | |||
| fb74eac976 | |||
| afbec4a656 | |||
| a4a4cb1cdb | |||
| c056e99130 | |||
| de6fb6e2f7 | |||
| 5e7440acdc | |||
| 5de37ea321 | |||
| cf01fd6bef | |||
| 034c9d9559 | |||
| daa896b54a | |||
| bccc540bce | |||
| 3071a94206 | |||
| 493bef4270 | |||
| 4ec1c56fd6 | |||
| d1d193c61a | |||
| 1f037c1abb | |||
| 38fc785fe4 | |||
| fc377530a2 | |||
| 53176786c8 | |||
| 075138c474 | |||
| 18023c7dab | |||
| 22ae648dd2 | |||
| fbf4d35827 | |||
| 7c9a625cf8 | |||
| ca0020a224 | |||
| 0c4a148cdb | |||
| 6294b06174 | |||
| 1d8c25f479 | |||
| 7f01b0369a | |||
| a7a85820f2 | |||
| 63f7915427 | |||
| b7b1e20ed4 | |||
| 8657868c97 | |||
| 9ae3cd1be7 | |||
| 829c469c5b | |||
| 3fdae1298f | |||
| e67ae9fe63 | |||
| f0f1caa67d | |||
| ee06a613b3 | |||
| bc0c77d35c | |||
| 8c41baf328 | |||
| 355e8b6cf9 | |||
| fcbe53778d | |||
| f1bb6ea00c | |||
| 013d4e7303 | |||
| 0f4478b8e5 | |||
| 5dfcc1aedd | |||
| aa4baad9a4 | |||
| 1c530d86d1 | |||
| 88d342fdf2 | |||
| c96efdb613 | |||
| 06e13d72f0 | |||
| 336f311676 | |||
| 211c8caca5 | |||
| ce8ec44c40 | |||
| d7a0cb8d00 | |||
| 7cec5c9b71 | |||
| 0b3802bb0b | |||
| 37c7b5004b | |||
| 05305ea751 | |||
| 5cb5826c45 | |||
| a5e94e0a08 | |||
| aa2919309f | |||
| 1c42deaf58 | |||
| 7d01ea90fe | |||
| e7a3a5982e | |||
| 74d3c96149 | |||
| ef94d46a7f | |||
| d537ffbeb5 | |||
| 65afe1196a | |||
| 23ec91a2c4 | |||
| e4b036da69 | |||
| 4a454da7ce | |||
| 7e5bd6d54e | |||
| fff106ffc2 | |||
| b6470d6252 | |||
| d95a5de2ac | |||
| b14d02d177 | |||
| 93aac92d01 | |||
| 1e09e6ffa6 | |||
| 7e170eb1b3 | |||
| 8e9abf5bd6 | |||
| d238e4fd99 | |||
| d3faf37e8b | |||
| 0f32a8b13d | |||
| 6f55bc72a9 | |||
| 443656c85c | |||
| aed27a6b69 | |||
| 5e6a2dc020 | |||
| 37f5032db5 | |||
| 378a2e3f32 | |||
| bda0176205 | |||
| 1cc3736947 | |||
| 1e7b4f6537 | |||
| b709cc3eee | |||
| 03102060dd | |||
| 1c31e9722f | |||
| a42516478d | |||
| 02300131b7 | |||
| 28f91d0b2d | |||
| 40dbc075d8 | |||
| 7050d41112 | |||
| 609e18fbc3 | |||
| c038bc3d09 | |||
| 1c31846b97 | |||
| a0739d133d | |||
| 416a586a1d | |||
| 3b8bde71fa | |||
| 0dfe161746 | |||
| 2bc19b944e | |||
| 5c0e2639d4 | |||
| 45ad566de9 | |||
| 5fc8c89068 | |||
| eac4b571ee | |||
| e2e8268afb | |||
| e70e73d9b7 | |||
| 7ad4a82cb3 | |||
| 2c757362db | |||
| bf505f1075 | |||
| e81ce235b6 | |||
| a99c603cba | |||
| d424951ce0 | |||
| 585eb7d2e8 | |||
| 745c87c8a3 | |||
| 293ccf1948 | |||
| fa534ac13e | |||
| be7316f99e | |||
| f6c86bbd1b | |||
| d03306a1ce | |||
| 6d2a2edc34 | |||
| a30f476d18 | |||
| 81fafbc199 | |||
| 842dd9f729 | |||
| a734d9caba | |||
| 5fdf46bbca | |||
| 8912f31a06 | |||
| 1cb9abbb90 | |||
| 1782190cbe | |||
| e0fc0dc15b | |||
| 0b82683d9d | |||
| b9ca2c5320 | |||
| 4e9c9df95a | |||
| afd4ccf425 | |||
| b66a627a00 | |||
| 0f235f56c1 | |||
| 36e03243da | |||
| 7fe8b9e448 | |||
| 639dceb19b | |||
| db8d3fef9c | |||
| 14ce932d9a | |||
| 6cde2b3bc0 | |||
| aec51de205 | |||
| aba3268336 | |||
| fde6c3c7ef | |||
| 0ded712a6b | |||
| a28e42c0c1 | |||
| 2fe254dac4 | |||
| 24b0aa122e | |||
| b7d4c3cef2 | |||
| f1feb6fcd6 | |||
| d56573e41c | |||
| 7fd5907b8d | |||
| d365aef387 | |||
| dcff2a039f | |||
| 189464ade2 | |||
| c01663867a | |||
| bf0849d9a4 | |||
| 1c2004265c | |||
| 90743958a7 | |||
| c91ee7412e | |||
| 7e076e4dc8 | |||
| 6b8243f6af | |||
| f7d052160f | |||
| 49fcf4e984 | |||
| 8e874c5ad6 | |||
| b710b9b222 | |||
| 4659884ef4 | |||
| c0a6ce3bed | |||
| 4a339ac3e5 | |||
| b2da7b06a3 | |||
| 3ec1c77fc8 | |||
| 933404f2eb | |||
| 9c7ca927d3 | |||
| 970fe4d26d | |||
| b89e60af09 | |||
| 6784ab5334 | |||
| e644d854f3 | |||
| f9e36cc9f1 | |||
| 91af0db62a | |||
| 1a980f3e43 | |||
| ab1fcc6db4 | |||
| 994a21d4ff | |||
| 5bca23a73d | |||
| 9bae536652 | |||
| 151e1e8d42 | |||
| 6d68f5ba61 | |||
| 01f67edc81 | |||
| e3d6973ed2 | |||
| 381de34d2a | |||
| 8e3b63a32a | |||
| 33f4a94841 | |||
| 52854e2805 | |||
| 2fe3312360 | |||
| 4abf80757d | |||
| 6ed4268891 | |||
| 6cd156fdbc | |||
| 3a1443ee0b | |||
| c108c1ead7 | |||
| 5c9a7231d7 | |||
| 1ad5ef6c30 | |||
| e2ffae6fb6 | |||
| a5e26adf33 | |||
| 228c50444d | |||
| 997ca5d78e | |||
| 2abf7ccd5f | |||
| c3c25c7899 | |||
| 911bbea72e | |||
| 885a65b33a | |||
| f89f680035 | |||
| 8e08798e8e | |||
| 064450ce1f | |||
| 4d1aaab4c0 | |||
| 7576c307fb | |||
| f3f10bfab6 | |||
| e7bf300cdf | |||
| f357b3f715 | |||
| 4038a3419b | |||
| fe76a2562e | |||
| 6f9d8ca34f | |||
| 1bcd7533c4 | |||
| ff5f100601 | |||
| 248742095d | |||
| 6219f4d2ac | |||
| b0cd9cac9a | |||
| be997d91fa | |||
| 77c0a7cf05 | |||
| b26a2d7328 | |||
| 3f8fb93834 | |||
| f278206b91 | |||
| 3905247a84 | |||
| 4414c4598e | |||
| 897436f056 | |||
| c68e966686 | |||
| f7ed5663a7 | |||
| 8f13279477 | |||
| f50245a6d3 | |||
| c1b7ab67c7 | |||
| 9c195964aa | |||
| 39af71da2c | |||
| 558de0b8b4 | |||
| b56fe5fff9 | |||
| 716113cbfb | |||
| 9eac2f3c93 | |||
| 701e4cc18d | |||
| 104be7a8a7 | |||
| adf6b7bf7f | |||
| aaf858c6db | |||
| a8fa0e61ac | |||
| 9287d34b78 | |||
| 115b8e9a6c | |||
| 39fcdac6d3 | |||
| 90d0f55fe1 | |||
| 6c1b457252 | |||
| b0c9e538e4 | |||
| 5e968b8864 | |||
| ae17dcde01 | |||
| 191ac5c44c | |||
| 2eca6f3d5f | |||
| 42d864754a | |||
| 7653886081 | |||
| c86674c04d | |||
| ab85e38732 | |||
| c847d5ea85 | |||
| 30fa519fa0 | |||
| 11a3328b06 | |||
| dac3242a2c | |||
| 5f20a1efc2 | |||
| e258e86f15 | |||
| 5e3470fd4a | |||
| c93271e1d6 | |||
| 0ed448a277 | |||
| ad241529f2 | |||
| 63d96ee2d8 | |||
| 4f3822bba9 | |||
| e4bbd79059 | |||
| fe2f6bbe27 | |||
| 7f489efdde | |||
| a0ce4d0c52 | |||
| f7f0ab2403 | |||
| 3fcfe24a7e | |||
| 194baa7436 | |||
| ffdb463148 | |||
| ec0c5414b2 | |||
| 7f1caa7c57 | |||
| 75b3874f97 | |||
| 7d5ff8512a | |||
| 8b009ba789 | |||
| 28f306aac9 | |||
| ab38bff9df | |||
| 9892a5c5fc | |||
| 5ac04b4066 | |||
| b83ed4284e | |||
| 119dc62c26 | |||
| c57f73b11f | |||
| 78c1fbac60 | |||
| 71b2e366cd | |||
| ffce63d647 | |||
| 7a03a909a1 | |||
| f443edd43e | |||
| 120e0af3de | |||
| a99bebe766 | |||
| c58a4359e8 | |||
| d48e967e89 | |||
| 09414b1184 | |||
| 4826318d38 | |||
| 2b4ddd43ae | |||
| c8f80a6e57 | |||
| 1db97f7655 | |||
| acbc1e2f19 | |||
| a57d584b63 | |||
| ef0e06808a | |||
| eeff8efe54 | |||
| 6bfbf54f23 | |||
| 6038897710 | |||
| e3ec5309f6 | |||
| d3f1b1ccbe | |||
| e00b9ca316 |
@@ -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();
|
||||
},
|
||||
};
|
||||
@@ -155,16 +157,6 @@ module.exports = grunt => {
|
||||
],
|
||||
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>
|
||||
|
||||
@@ -38,6 +38,19 @@ if (!is_siteadmin()) {
|
||||
}
|
||||
|
||||
$admisselector = new core_role_admins_existing_selector();
|
||||
|
||||
if (array_key_exists('siteadmins', $CFG->config_php_settings)) {
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->heading(get_string('manageadmins', 'core_role'), 3);
|
||||
echo $OUTPUT->notification(get_string('siteadministratorsconfigphp', 'core_role'), \core\output\notification::NOTIFY_INFO);
|
||||
echo $OUTPUT->box_start();
|
||||
echo $OUTPUT->paragraph(get_string('existingadmins', 'core_role'));
|
||||
$admisselector->display();
|
||||
echo $OUTPUT->box_end();
|
||||
echo $OUTPUT->footer();
|
||||
die();
|
||||
}
|
||||
|
||||
$potentialadmisselector = new core_role_admins_potential_selector();
|
||||
|
||||
if ($addusersaction) {
|
||||
|
||||
@@ -41,4 +41,18 @@ if (!empty($defaulth5plib)) {
|
||||
|
||||
$settings->add(new admin_settings_h5plib_handler_select('h5plibraryhandler', new lang_string('h5plibraryhandler', 'core_h5p'),
|
||||
new lang_string('h5plibraryhandler_help', 'core_h5p'), $defaulth5plib));
|
||||
|
||||
$setting = new admin_setting_configtextarea(
|
||||
'core_h5p/h5pcustomcss',
|
||||
new lang_string('h5pcustomcss', 'core_h5p'),
|
||||
new lang_string('h5pcustomcss_help', 'core_h5p'),
|
||||
'',
|
||||
PARAM_NOTAGS
|
||||
);
|
||||
$setting->set_updatedcallback(function () {
|
||||
// Enables use of file_storage constants.
|
||||
\core_h5p\local\library\autoloader::register();
|
||||
\core_h5p\file_storage::generate_custom_styles();
|
||||
});
|
||||
$settings->add($setting);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,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);
|
||||
|
||||
@@ -47,7 +47,7 @@ $string['cachedef_purpose'] = 'Data purposes';
|
||||
$string['cachedef_purpose_overrides'] = 'Purpose overrides in the Data privacy tool';
|
||||
$string['cachedef_contextlevel'] = 'Context levels purpose and category';
|
||||
$string['cancelrequest'] = 'Cancel request';
|
||||
$string['cancelrequestconfirmation'] = 'Do you really want cancel this data request?';
|
||||
$string['cancelrequestconfirmation'] = 'Do you want to cancel this data request?';
|
||||
$string['cannotreset'] = 'Unable to reset this request. Only rejected requests can be reset.';
|
||||
$string['categories'] = 'Categories';
|
||||
$string['category'] = 'Category';
|
||||
|
||||
@@ -75,7 +75,7 @@ class manage_competency_frameworks_page implements renderable, templatable {
|
||||
);
|
||||
$this->navigation[] = $addpage;
|
||||
$competenciesrepository = new single_button(
|
||||
new moodle_url('https://archive.moodle.net/competencies'),
|
||||
new moodle_url('https://moodle.net/search', ['q' => 'competency frameworks']),
|
||||
get_string('competencyframeworksrepository', 'tool_lp'),
|
||||
'get'
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
}
|
||||
}}
|
||||
<span>
|
||||
<img height="12" src="{{profileimageurlsmall}}" alt="" role="presentation">
|
||||
<img height="12" src="{{profileimageurlsmall}}" alt="">
|
||||
<span>{{fullname}}</span>
|
||||
{{#hasidentity}}
|
||||
<span><small>{{identity}}</small></span>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<p>
|
||||
<ul class="inline list-inline">
|
||||
{{#coursemodules}}
|
||||
<li class="list-inline-item"><a href="{{url}}"><img class="icon" src="{{iconurl}}"> {{name}} </a></li>
|
||||
<li class="list-inline-item"><a href="{{url}}"><img class="icon" src="{{iconurl}}" alt=""> {{name}} </a></li>
|
||||
{{/coursemodules}}
|
||||
{{^coursemodules}}
|
||||
<li class="list-inline-item"><span class="alert">{{#str}}noactivities, tool_lp{{/str}}</span></li>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
}}
|
||||
<span>
|
||||
<a href="{{profileurl}}" title="{{#str}}viewprofile{{/str}}">
|
||||
<img height="18" src="{{profileimageurlsmall}}" alt="" role="presentation">
|
||||
<img height="18" src="{{profileimageurlsmall}}" alt="">
|
||||
<span>{{fullname}}</span>
|
||||
{{#hasidentity}}
|
||||
<span><small>{{identity}}</small></span>
|
||||
|
||||
@@ -42,7 +42,20 @@ $PAGE->set_title($pagetitle);
|
||||
|
||||
// Logout if it was requested.
|
||||
$logout = optional_param('logout', false, PARAM_BOOL);
|
||||
$sesskey = optional_param('sesskey', '_none_', PARAM_RAW);
|
||||
|
||||
if ($logout) {
|
||||
if (!confirm_sesskey($sesskey)) {
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->confirm(
|
||||
get_string('logoutconfirm'),
|
||||
new moodle_url($PAGE->url, ['logout' => 1, 'sesskey' => sesskey()]),
|
||||
new moodle_url('/'),
|
||||
);
|
||||
echo $OUTPUT->footer();
|
||||
die;
|
||||
}
|
||||
|
||||
if (!empty($SESSION->wantsurl)) {
|
||||
// If we have the wantsurl, we should redirect there, to keep it intact.
|
||||
$wantsurl = $SESSION->wantsurl;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?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_mfa\hook;
|
||||
|
||||
use core\hook\described_hook;
|
||||
use core\hook\stoppable_trait;
|
||||
|
||||
/**
|
||||
* Allow plugins to callback as soon possible after user has passed MFA.
|
||||
*
|
||||
* @package tool_mfa
|
||||
* @copyright 2024 Juan Leyva
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class after_user_passed_mfa implements
|
||||
described_hook,
|
||||
\Psr\EventDispatcher\StoppableEventInterface {
|
||||
use stoppable_trait;
|
||||
|
||||
/**
|
||||
* Describes the hook purpose.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_hook_description(): string {
|
||||
return 'Allow plugins to callback as soon possible after user has passed MFA.';
|
||||
}
|
||||
|
||||
/**
|
||||
* List of tags that describe this hook.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public static function get_hook_tags(): array {
|
||||
return ['login'];
|
||||
}
|
||||
}
|
||||
@@ -332,6 +332,10 @@ class manager {
|
||||
$event = \tool_mfa\event\user_passed_mfa::user_passed_mfa_event($USER);
|
||||
$event->trigger();
|
||||
|
||||
// Allow plugins to callback as soon possible after user has passed MFA.
|
||||
$hook = new \tool_mfa\hook\after_user_passed_mfa();
|
||||
\core\hook\manager::get_instance()->dispatch($hook);
|
||||
|
||||
// Add/update record in DB for users last mfa auth.
|
||||
self::update_pass_time();
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
namespace tool_mfa\output;
|
||||
|
||||
use core\context\system;
|
||||
use tool_mfa\local\factor\object_factor;
|
||||
use tool_mfa\local\form\login_form;
|
||||
use \html_writer;
|
||||
@@ -222,7 +223,8 @@ class renderer extends \plugin_renderer_base {
|
||||
// Support link.
|
||||
$supportemail = $CFG->supportemail;
|
||||
if (!empty($supportemail)) {
|
||||
$subject = get_string('email:subject', 'tool_mfa', $SITE->fullname);
|
||||
$subject = get_string('email:subject', 'tool_mfa',
|
||||
format_string($SITE->fullname, true, ['context' => system::instance()]));
|
||||
$maillink = \html_writer::link("mailto:$supportemail?Subject=$subject", $supportemail);
|
||||
$notification .= get_string('error:support', 'tool_mfa');
|
||||
$notification .= \html_writer::tag('p', $maillink);
|
||||
@@ -635,6 +637,7 @@ class renderer extends \plugin_renderer_base {
|
||||
'hasadditionalfactors' => $hasadditionalfactors,
|
||||
'additionalfactors' => $alladitionalfactors,
|
||||
'authurl' => $authurl->out(),
|
||||
'sesskey' => sesskey(),
|
||||
'supportlink' => $this->get_support_link(),
|
||||
'disablefactor' => $disablefactor
|
||||
];
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
*/
|
||||
// Ignore coding standards for login check, this page does not require login.
|
||||
// @codingStandardsIgnoreStart
|
||||
require_once(__DIR__ . '../../../../../../config.php');
|
||||
require_once(__DIR__ . '/../../../../../config.php');
|
||||
$instanceid = required_param('instance', PARAM_INT);
|
||||
$pass = optional_param('pass', '0', PARAM_INT);
|
||||
$secret = optional_param('secret', 0, PARAM_INT);
|
||||
|
||||
@@ -31,8 +31,7 @@ require_once($CFG->libdir.'/tablelib.php');
|
||||
require_login(null, false);
|
||||
require_capability('moodle/site:config', context_system::instance());
|
||||
|
||||
global $_SERVER;
|
||||
$returnurl = $_SERVER['HTTP_REFERER'];
|
||||
$returnurl = get_local_referer(false);
|
||||
|
||||
$PAGE->set_url('/admin/tool/mfa/index.php');
|
||||
|
||||
|
||||
@@ -112,6 +112,10 @@ function tool_mfa_after_config(): void {
|
||||
* @return array
|
||||
*/
|
||||
function tool_mfa_bulk_user_actions(): array {
|
||||
if (!has_capability('moodle/site:config', context_system::instance())) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'tool_mfa_reset_factors' => new action_link(
|
||||
new moodle_url('/admin/tool/mfa/reset_factor.php'),
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
<div class="d-flex justify-content-center">
|
||||
<form id="cancelmfa" method="POST" action="{{{authurl}}}">
|
||||
<input type="hidden" id="{{uniqid}}-logout" name="logout" value="true">
|
||||
<input type="hidden" name="sesskey" value="{{sesskey}}">
|
||||
<input type="submit" class="btn btn-secondary"
|
||||
name="cancelmfa" id="cancelmfa_button"
|
||||
value="{{#str}} cancellogin, tool_mfa {{/str}}">
|
||||
|
||||
@@ -388,6 +388,14 @@ class api {
|
||||
$CFG->searchbanner, FORMAT_HTML, $context)[0];
|
||||
}
|
||||
|
||||
if (empty($section) || $section === 'h5psettings') {
|
||||
\core_h5p\local\library\autoloader::register();
|
||||
$customcss = \core_h5p\file_storage::get_custom_styles();
|
||||
if (!empty($customcss)) {
|
||||
$settings->h5pcustomcssurl = $customcss['cssurl']->out() . '?ver=' . $customcss['cssversion'];
|
||||
}
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Allows plugins to add any elements to the footer.
|
||||
*
|
||||
* @package tool_mobile
|
||||
* @copyright 2024 Juan Leyva
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class hook_callbacks {
|
||||
/**
|
||||
* Callback to recover $SESSION->wantsurl.
|
||||
*
|
||||
* @param \core_user\hook\after_login_completed $hook
|
||||
*/
|
||||
public static function after_login_completed(
|
||||
\core_user\hook\after_login_completed $hook,
|
||||
): void {
|
||||
global $SESSION, $CFG;
|
||||
|
||||
// Check if the user is doing a mobile app launch, if that's the case, ensure $SESSION->wantsurl is correctly set.
|
||||
if (!NO_MOODLE_COOKIES && !empty($_COOKIE['tool_mobile_launch'])) {
|
||||
if (empty($SESSION->wantsurl) || strpos($SESSION->wantsurl, '/tool/mobile/launch.php') === false) {
|
||||
$params = json_decode($_COOKIE['tool_mobile_launch'], true);
|
||||
$SESSION->wantsurl = (new \moodle_url("/$CFG->admin/tool/mobile/launch.php", $params))->out(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 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'],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?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\local\hooks\user;
|
||||
|
||||
/**
|
||||
* Handles mobile app launches when third-party auth plugins are put in front of MFA.
|
||||
*
|
||||
* @package tool_mobile
|
||||
* @copyright 2024 Juan Leyva
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class after_user_passed_mfa {
|
||||
/**
|
||||
* Callback to recover $SESSION->wantsurl.
|
||||
*
|
||||
* @param \tool_mfa\hook\after_user_passed_mfa $hook
|
||||
*/
|
||||
public static function callback(\tool_mfa\hook\after_user_passed_mfa $hook): void {
|
||||
global $SESSION, $CFG;
|
||||
|
||||
// Check if the user is doing a mobile app launch, if that's the case, ensure $SESSION->wantsurl is correctly set.
|
||||
if (!NO_MOODLE_COOKIES && !empty($_COOKIE['tool_mobile_launch'])) {
|
||||
if (empty($SESSION->wantsurl) || strpos($SESSION->wantsurl, '/tool/mobile/launch.php') === false) {
|
||||
$params = json_decode($_COOKIE['tool_mobile_launch'], true);
|
||||
$SESSION->wantsurl = (new \moodle_url("/$CFG->admin/tool/mobile/launch.php", $params))->out(false);
|
||||
$SESSION->tool_mfa_has_been_redirected = true; // Indicate MFA that they need to follow $SESSION->wantsurl.
|
||||
}
|
||||
// Invalidate cookie as we won't be needing it anymore.
|
||||
unset($_COOKIE['tool_mobile_launch']);
|
||||
if (!headers_sent()) { // Just be very cautios as this is a critical code.
|
||||
setcookie('tool_mobile_launch', '', -1, $CFG->sessioncookiepath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Hook callbacks for Moodle app tools
|
||||
*
|
||||
* @package tool_mobile
|
||||
* @copyright 2024 Juan Leyva
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$callbacks = [
|
||||
[
|
||||
'hook' => \core_user\hook\after_login_completed::class,
|
||||
'callback' => \tool_mobile\hook_callbacks::class . '::after_login_completed',
|
||||
'priority' => 500,
|
||||
],
|
||||
[
|
||||
'hook' => tool_mfa\hook\after_user_passed_mfa::class,
|
||||
'callback' => 'tool_mobile\local\hooks\user\after_user_passed_mfa::callback',
|
||||
'priority' => 500,
|
||||
],
|
||||
];
|
||||
@@ -44,6 +44,18 @@ if (!$CFG->enablewebservices) {
|
||||
throw new moodle_exception('enablewsdescription', 'webservice');
|
||||
}
|
||||
|
||||
// Check if the service exists and is enabled.
|
||||
$service = $DB->get_record('external_services', ['shortname' => $serviceshortname, 'enabled' => 1]);
|
||||
if (empty($service)) {
|
||||
throw new moodle_exception('servicenotavailable', 'webservice');
|
||||
}
|
||||
|
||||
// Set a cookie indicating that there was a launch to authenticate via the site from the app.
|
||||
$ldata = json_encode(['service' => $serviceshortname, 'passport' => $passport,
|
||||
'urlscheme' => $urlscheme, 'confirmed' => (int) $confirmed, 'oauthsso' => $oauthsso]);
|
||||
$expires = time() + (15 * MINSECS); // 15 minutes for authentication should be enough.
|
||||
setcookie('tool_mobile_launch', $ldata, $expires, $CFG->sessioncookiepath, $CFG->sessioncookiedomain);
|
||||
|
||||
// We have been requested to start a SSO process via OpenID.
|
||||
if (!empty($oauthsso) && is_enabled_auth('oauth2')) {
|
||||
$wantsurl = new moodle_url('/admin/tool/mobile/launch.php',
|
||||
@@ -63,17 +75,15 @@ if (empty($SESSION->justloggedin) &&
|
||||
throw new moodle_exception('pluginnotenabledorconfigured', 'tool_mobile');
|
||||
}
|
||||
|
||||
// Check if the service exists and is enabled.
|
||||
$service = $DB->get_record('external_services', array('shortname' => $serviceshortname, 'enabled' => 1));
|
||||
if (empty($service)) {
|
||||
throw new moodle_exception('servicenotavailable', 'webservice');
|
||||
}
|
||||
|
||||
require_login(0, false);
|
||||
|
||||
// Require an active user: not guest, not suspended.
|
||||
core_user::require_active_user($USER);
|
||||
|
||||
// Remove cookie.
|
||||
unset($_COOKIE['tool_mobile_launch']);
|
||||
setcookie('tool_mobile_launch', '', -1, $CFG->sessioncookiepath);
|
||||
|
||||
// Get an existing token or create a new one.
|
||||
$timenow = time();
|
||||
$token = \core_external\util::generate_token_for_current_user($service);
|
||||
@@ -126,7 +136,7 @@ if ($confirmed or $isios) {
|
||||
}
|
||||
|
||||
$notice = get_string('clickheretolaunchtheapp', 'tool_mobile');
|
||||
echo html_writer::link($location, $notice, array('id' => 'launchapp'));
|
||||
echo $OUTPUT->box(html_writer::link($location, $notice, ['id' => 'launchapp']), 'generalbox warning centerpara');
|
||||
echo html_writer::script(
|
||||
"window.onload = function() {
|
||||
document.getElementById('launchapp').click();
|
||||
|
||||
@@ -271,3 +271,15 @@ 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']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,6 +270,18 @@ class externallib_test extends externallib_advanced_testcase {
|
||||
$this->assertCount(0, $result['warnings']);
|
||||
$this->assertEquals($expected, $result['settings']);
|
||||
|
||||
// H5P custom CSS.
|
||||
set_config('h5pcustomcss', '.debug { color: #fab; }', 'core_h5p');
|
||||
\core_h5p\local\library\autoloader::register();
|
||||
\core_h5p\file_storage::generate_custom_styles();
|
||||
$result = external::get_config();
|
||||
$result = external_api::clean_returnvalue(external::get_config_returns(), $result);
|
||||
|
||||
$customcss = \core_h5p\file_storage::get_custom_styles();
|
||||
$expected[] = ['name' => 'h5pcustomcssurl', 'value' => $customcss['cssurl']->out() . '?ver=' . $customcss['cssversion']];
|
||||
$this->assertCount(0, $result['warnings']);
|
||||
$this->assertEquals($expected, $result['settings']);
|
||||
|
||||
// Change a value and retrieve filtering by section.
|
||||
set_config('commentsperpage', 1);
|
||||
$expected[10]['value'] = 1;
|
||||
|
||||
@@ -688,6 +688,17 @@ class process {
|
||||
break;
|
||||
|
||||
case UU_USER_ADD_UPDATE:
|
||||
if ($this->get_match_on_email()) {
|
||||
if ($usersbyname = $DB->get_records('user', ['username' => $user->username])) {
|
||||
foreach ($usersbyname as $userbyname) {
|
||||
if (strtolower($userbyname->email) != strtolower($user->email)) {
|
||||
$this->usersskipped++;
|
||||
$this->upt->track('status', get_string('usernotaddedusernameexists', 'error'), 'warning');
|
||||
$skip = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case UU_USER_UPDATE:
|
||||
|
||||
@@ -50,7 +50,7 @@ The example text file may also be used for testing, as you are able to preview u
|
||||
$string['infoprefix'] = 'Info:';
|
||||
$string['invalidupdatetype'] = 'This option cannot be selected with the chosen upload type.';
|
||||
$string['invaliduserdata'] = 'Invalid data detected for user {$a} and it has been automatically cleaned.';
|
||||
$string['invaliduserdatavalues'] = 'Invalid data detected for user \'{$a->username}\' ({$a->values}), which has been automatically cleaned.';
|
||||
$string['invaliduserdatavalues'] = 'Incorrect data ({$a->values}) found for user {$a->username}. This data has been corrected or deleted.';
|
||||
$string['invalidtheme'] = 'Theme "{$a}" is not installed and will be ignored.';
|
||||
$string['linex'] = 'Line {$a}';
|
||||
$string['matchemail'] = 'Match on email address';
|
||||
|
||||
@@ -296,3 +296,25 @@ Feature: Upload users
|
||||
And I navigate to "Users > Accounts > Browse list of users" in site administration
|
||||
And I should see "Bilbo Baggins"
|
||||
And I should not see "Frodo Baggins"
|
||||
|
||||
@javascript
|
||||
Scenario: Create a new user when matching them on email where where the username already exists
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| bilbob | Samwise | Gamgee | samwise@example.com |
|
||||
| frodob | Frodeo | Baspins | frodo@example.com |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Users > Accounts > Upload users" in site administration
|
||||
When I upload "lib/tests/fixtures/upload_users_email_matching.csv" file to "File" filemanager
|
||||
And I press "Upload users"
|
||||
Then I should see "Upload users preview"
|
||||
And I set the following fields to these values:
|
||||
| Upload type | Add new and update existing users |
|
||||
| Existing user details | Override with file |
|
||||
| Match on email address | Yes |
|
||||
And I press "Upload users"
|
||||
And I should see "User not added - username already exists under a different email"
|
||||
And I press "Continue"
|
||||
And I navigate to "Users > Accounts > Browse list of users" in site administration
|
||||
And I should see "Samwise Gamgee"
|
||||
And I should see "Frodo Baggins"
|
||||
|
||||
@@ -202,8 +202,8 @@ EOF;
|
||||
|
||||
// We should get the debugging from the user class itself, as well as warning in the output regarding the same.
|
||||
$this->assertDebuggingCalled('The property \'country\' has invalid data and has been cleaned.');
|
||||
$this->assertStringContainsString('Invalid data detected for user \'student1\' (country), ' .
|
||||
'which has been automatically cleaned.', $output);
|
||||
$this->assertStringContainsString('Incorrect data (country) found for user student1. ' .
|
||||
'This data has been corrected or deleted.', $output);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -283,7 +283,7 @@ class manager {
|
||||
'title' => get_string('importtour', 'tool_usertours'),
|
||||
],
|
||||
(object) [
|
||||
'link' => new \moodle_url('https://archive.moodle.net/tours'),
|
||||
'link' => new \moodle_url('https://moodle.net/search', ['q' => 'user tours']),
|
||||
'linkproperties' => [
|
||||
'target' => '_blank',
|
||||
],
|
||||
|
||||
@@ -91,12 +91,12 @@ class edit_field_save extends XMLDBAction {
|
||||
$comment = trim($comment);
|
||||
|
||||
$type = required_param('type', PARAM_INT);
|
||||
$length = strtolower(optional_param('length', NULL, PARAM_ALPHANUM));
|
||||
$length = optional_param('length', null, PARAM_INT);
|
||||
$decimals = optional_param('decimals', NULL, PARAM_INT);
|
||||
$notnull = optional_param('notnull', false, PARAM_BOOL);
|
||||
$sequence = optional_param('sequence', false, PARAM_BOOL);
|
||||
$default = optional_param('default', NULL, PARAM_PATH);
|
||||
$default = trim($default);
|
||||
$default = is_null($default) ? $default : trim($default);
|
||||
|
||||
$editeddir = $XMLDB->editeddirs[$dirpath];
|
||||
$structure = $editeddir->xml_file->getStructure();
|
||||
|
||||
+1
-1
@@ -115,7 +115,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'] = 'Log 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 = 2023100900; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->version = 2023100901; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->requires = 2023100400; // Requires this Moodle version.
|
||||
$plugin->component = 'auth_lti'; // Full name of the plugin (used for diagnostics).
|
||||
|
||||
@@ -49,6 +49,9 @@ abstract class info {
|
||||
/** @var tree Availability configuration, decoded from JSON; null if unset */
|
||||
protected $availabilitytree;
|
||||
|
||||
/** @var array The groups the current user belongs to. */
|
||||
protected $groups;
|
||||
|
||||
/** @var array|null Array of information about current restore if any */
|
||||
protected static $restoreinfo = null;
|
||||
|
||||
@@ -65,6 +68,7 @@ abstract class info {
|
||||
$this->course = $course;
|
||||
$this->visible = (bool)$visible;
|
||||
$this->availability = $availability;
|
||||
$this->groups = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -800,4 +804,28 @@ abstract class info {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns groups that the current user belongs to on the course. Note: If not already
|
||||
* available, this may make a database query.
|
||||
*
|
||||
* This will include groups the user is not allowed to see themselves, so check visibility
|
||||
* before displaying groups to the user.
|
||||
*
|
||||
* @param int $groupingid Grouping ID or 0 (default) for all groups
|
||||
* @return int[] Array of int (group id) => int (same group id again); empty array if none
|
||||
*/
|
||||
public function get_groups(int $groupingid = 0): array {
|
||||
global $USER;
|
||||
if (is_null($this->groups)) {
|
||||
$allgroups = groups_get_user_groups($this->course->id, $USER->id, true);
|
||||
$this->groups = $allgroups;
|
||||
} else {
|
||||
$allgroups = $this->groups;
|
||||
}
|
||||
if (!isset($allgroups[$groupingid])) {
|
||||
return [];
|
||||
}
|
||||
return $allgroups[$groupingid];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ class condition extends \core_availability\condition {
|
||||
$allow = true;
|
||||
if (!has_capability('moodle/site:accessallgroups', $context, $userid)) {
|
||||
// Get all groups the user belongs to.
|
||||
$groups = $info->get_modinfo()->get_groups();
|
||||
$groups = $info->get_groups();
|
||||
if ($this->groupid) {
|
||||
$allow = in_array($this->groupid, $groups);
|
||||
} else {
|
||||
|
||||
@@ -108,3 +108,49 @@ Feature: availability_group
|
||||
When I am on the "C1" "Course" page logged in as "student1"
|
||||
Then I should see "Not available unless: You belong to G-One"
|
||||
And I should not see "G-Un"
|
||||
|
||||
@javascript
|
||||
Scenario: Condition using a hidden group
|
||||
Given the following "groups" exist:
|
||||
| name | course | idnumber | visibility |
|
||||
| Hidden Group | C1 | GA | 3 |
|
||||
And I log in as "teacher1"
|
||||
And I add a page activity to course "Course 1" section "1"
|
||||
And I expand all fieldsets
|
||||
|
||||
# Page P1 any group.
|
||||
And I am on the "P1" "page activity editing" page
|
||||
And I expand all fieldsets
|
||||
And I click on "Add restriction..." "button"
|
||||
And "Group" "button" should exist in the "Add restriction..." "dialogue"
|
||||
And I click on "Group" "button" in the "Add restriction..." "dialogue"
|
||||
And I set the field "Group" to "(Any group)"
|
||||
And I click on ".availability-item .availability-eye img" "css_element"
|
||||
And I click on "Save and return to course" "button"
|
||||
|
||||
# Page P2 with hidden group.
|
||||
And I am on the "P2" "page activity editing" page
|
||||
And I expand all fieldsets
|
||||
And I click on "Add restriction..." "button"
|
||||
And I click on "Group" "button" in the "Add restriction..." "dialogue"
|
||||
And I set the field "Group" to "Hidden Group"
|
||||
And I click on "Save and return to course" "button"
|
||||
|
||||
# Log back in as student.
|
||||
When I am on the "Course 1" "course" page logged in as "student1"
|
||||
|
||||
# No pages should appear yet.
|
||||
Then I should not see "P1" in the "region-main" "region"
|
||||
And I should not see "P2" in the "region-main" "region"
|
||||
And I should not see "Hidden Group"
|
||||
|
||||
# Add to groups and log out/in again.
|
||||
And the following "group members" exist:
|
||||
| user | group |
|
||||
| student1 | GA |
|
||||
And I am on "Course 1" course homepage
|
||||
|
||||
# P1 (any groups) and P2 should show. The user should not see the hidden group mentioned anywhere.
|
||||
And I should see "P1" in the "region-main" "region"
|
||||
And I should see "P2" in the "region-main" "region"
|
||||
And I should not see "Hidden Group"
|
||||
|
||||
@@ -49,6 +49,7 @@ class condition_test extends \advanced_testcase {
|
||||
$course = $generator->create_course();
|
||||
$user = $generator->create_user();
|
||||
$generator->enrol_user($user->id, $course->id);
|
||||
$this->setUser($user);
|
||||
$info = new \core_availability\mock_info($course, $user->id);
|
||||
|
||||
// Make 2 test groups, one in a grouping and one not.
|
||||
@@ -70,7 +71,7 @@ class condition_test extends \advanced_testcase {
|
||||
// Add user to groups and refresh cache.
|
||||
groups_add_member($group1, $user);
|
||||
groups_add_member($group2, $user);
|
||||
get_fast_modinfo($course->id, 0, true);
|
||||
$info = new \core_availability\mock_info($course, $user->id);
|
||||
|
||||
// Recheck.
|
||||
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
|
||||
|
||||
@@ -168,6 +168,7 @@ class backup_course_task extends backup_task {
|
||||
$content = self::encode_links_helper($content, 'BADGESVIEWBYID', '/badges/view.php?type=2&id=');
|
||||
$content = self::encode_links_helper($content, 'USERINDEXVIEWBYID', '/user/index.php?id=');
|
||||
$content = self::encode_links_helper($content, 'PLUGINFILEBYCONTEXT', '/pluginfile.php/');
|
||||
$content = self::encode_links_helper($content, 'PLUGINFILEBYCONTEXTURLENCODED', '/pluginfile.php/', true);
|
||||
|
||||
return $content;
|
||||
}
|
||||
@@ -178,17 +179,26 @@ class backup_course_task extends backup_task {
|
||||
* @param string $name the name of this type of encoded link.
|
||||
* @param string $path the path that identifies this type of link, up
|
||||
* to the ?paramname= bit.
|
||||
* @param bool $urlencoded whether to use urlencode() before replacing the path.
|
||||
* @return string content with one type of link encoded.
|
||||
*/
|
||||
static private function encode_links_helper($content, $name, $path) {
|
||||
private static function encode_links_helper(string $content, string $name, string $path, bool $urlencoded = false) {
|
||||
global $CFG;
|
||||
// We want to convert both http and https links.
|
||||
$root = $CFG->wwwroot;
|
||||
$httpsroot = str_replace('http://', 'https://', $root);
|
||||
$httproot = str_replace('https://', 'http://', $root);
|
||||
|
||||
$httpsbase = preg_quote($httpsroot . $path, '/');
|
||||
$httpbase = preg_quote($httproot . $path, '/');
|
||||
$httpsbase = $httpsroot . $path;
|
||||
$httpbase = $httproot . $path;
|
||||
|
||||
if ($urlencoded) {
|
||||
$httpsbase = urlencode($httpsbase);
|
||||
$httpbase = urlencode($httpbase);
|
||||
}
|
||||
|
||||
$httpsbase = preg_quote($httpsbase, '/');
|
||||
$httpbase = preg_quote($httpbase, '/');
|
||||
|
||||
$return = preg_replace('/(' . $httpsbase . ')([0-9]+)/', '$@' . $name . '*$2@$', $content);
|
||||
$return = preg_replace('/(' . $httpbase . ')([0-9]+)/', '$@' . $name . '*$2@$', $return);
|
||||
|
||||
@@ -167,6 +167,7 @@ class restore_course_task extends restore_task {
|
||||
$rules[] = new restore_decode_rule('BADGESVIEWBYID', '/badges/view.php?type=2&id=$1', 'course');
|
||||
$rules[] = new restore_decode_rule('USERINDEXVIEWBYID', '/user/index.php?id=$1', 'course');
|
||||
$rules[] = new restore_decode_rule('PLUGINFILEBYCONTEXT', '/pluginfile.php/$1', 'context');
|
||||
$rules[] = new restore_decode_rule('PLUGINFILEBYCONTEXTURLENCODED', '/pluginfile.php/$1', 'context', true);
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
@@ -3793,7 +3793,7 @@ class restore_activity_competencies_structure_step extends restore_structure_ste
|
||||
// Sortorder is ignored by precaution, anyway we should walk through the records in the right order.
|
||||
$record = (object) $params;
|
||||
$record->ruleoutcome = $data->ruleoutcome;
|
||||
$record->overridegrade = $data->overridegrade;
|
||||
$record->overridegrade = $data->overridegrade ?? 0;
|
||||
$coursemodulecompetency = new \core_competency\course_module_competency(0, $record);
|
||||
$coursemodulecompetency->create();
|
||||
}
|
||||
@@ -5434,6 +5434,25 @@ class restore_move_module_questions_categories extends restore_execution_step {
|
||||
];
|
||||
$params += $categoryidparams;
|
||||
$DB->execute($sqlupdate, $params);
|
||||
|
||||
// As explained in {@see restore_quiz_activity_structure_step::process_quiz_question_legacy_instance()}
|
||||
// question_set_references relating to random questions restored from old backups,
|
||||
// which pick from context_module question_categores, will have been restored with the wrong questioncontextid.
|
||||
// So, now, we need to find those, and updated the questioncontextid.
|
||||
// We can only find them by picking apart the filter conditions, and seeign which categories they refer to.
|
||||
|
||||
// We need to check all the question_set_references belonging to this context_module.
|
||||
$references = $DB->get_records('question_set_references', ['usingcontextid' => $newcontext->newitemid]);
|
||||
foreach ($references as $reference) {
|
||||
$filtercondition = json_decode($reference->filtercondition);
|
||||
if (!empty($filtercondition->questioncategoryid) &&
|
||||
in_array($filtercondition->questioncategoryid, $categoryids)) {
|
||||
// This is one of ours, update the questionscontextid.
|
||||
$DB->set_field('question_set_references',
|
||||
'questionscontextid', $newcontext->newitemid,
|
||||
['id' => $reference->id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now set the parent id for the question categories that were in the top category in the course context
|
||||
@@ -6239,6 +6258,11 @@ trait restore_question_set_reference_data_trait {
|
||||
|
||||
if ($context = $this->get_mappingid('context', $data->questionscontextid)) {
|
||||
$data->questionscontextid = $context;
|
||||
} else {
|
||||
$this->log('question_set_reference with old id ' . $data->id .
|
||||
' referenced question context ' . $data->questionscontextid .
|
||||
' which was not included in the backup. Therefore, this has been ' .
|
||||
' restored with the old questionscontextid.', backup::LOG_WARNING);
|
||||
}
|
||||
|
||||
$filtercondition['cat'] = implode(',', [
|
||||
|
||||
@@ -44,7 +44,18 @@ class restore_decode_rule {
|
||||
|
||||
protected $cregexp; // Calculated regular expresion we'll be looking for matches
|
||||
|
||||
public function __construct($linkname, $urltemplate, $mappings) {
|
||||
/** @var bool $urlencoded Whether to use urlencode() on the final URL. */
|
||||
protected bool $urlencoded;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $linkname How the link has been encoded in backup (CHOICEVIEWBYID, COURSEVIEWBYID...)
|
||||
* @param string $urltemplate How the original URL looks like, with dollar placeholders
|
||||
* @param array|string $mappings Which backup_ids mappings do we need to apply for replacing the placeholders
|
||||
* @param bool $urlencoded Whether to use urlencode() on the final URL (defaults to false)
|
||||
*/
|
||||
public function __construct(string $linkname, string $urltemplate, $mappings, bool $urlencoded = false) {
|
||||
// Validate all the params are ok
|
||||
$this->mappings = $this->validate_params($linkname, $urltemplate, $mappings);
|
||||
$this->linkname = $linkname;
|
||||
@@ -52,6 +63,7 @@ class restore_decode_rule {
|
||||
$this->restoreid = 0;
|
||||
$this->sourcewwwroot = '';
|
||||
$this->targetwwwroot = ''; // yes, uses to be $CFG->wwwroot, and? ;-)
|
||||
$this->urlencoded = $urlencoded;
|
||||
$this->cregexp = $this->get_calculated_regexp();
|
||||
}
|
||||
|
||||
@@ -96,6 +108,9 @@ class restore_decode_rule {
|
||||
} else { // All mappings found, apply target values to the template
|
||||
$toreplace = str_replace($placeholdersarr, $mappingstargetarr, $toreplace);
|
||||
}
|
||||
if ($this->urlencoded) {
|
||||
$toreplace = urlencode($toreplace);
|
||||
}
|
||||
// Finally, perform the replacement in original content
|
||||
$content = str_replace($tosearch, $toreplace, $content);
|
||||
}
|
||||
|
||||
@@ -50,14 +50,18 @@ class backup_encode_content_test extends \basic_testcase {
|
||||
// HTTPS root and links of both types in content.
|
||||
$CFG->wwwroot = $httpsroot;
|
||||
$encoded = backup_course_task::encode_content_links(
|
||||
$httproot . '/course/view.php?id=123, ' .
|
||||
$httpsroot . '/course/view.php?id=123, ' .
|
||||
$httpsroot . '/grade/index.php?id=123, ' .
|
||||
$httpsroot . '/grade/report/index.php?id=123, ' .
|
||||
$httpsroot . '/badges/view.php?type=2&id=123 and ' .
|
||||
$httpsroot . '/user/index.php?id=123.');
|
||||
$httproot . '/course/view.php?id=123, ' .
|
||||
$httpsroot . '/course/view.php?id=123, ' .
|
||||
$httpsroot . '/grade/index.php?id=123, ' .
|
||||
$httpsroot . '/grade/report/index.php?id=123, ' .
|
||||
$httpsroot . '/badges/view.php?type=2&id=123, ' .
|
||||
$httpsroot . '/user/index.php?id=123, ' .
|
||||
$httpsroot . '/pluginfile.php/123 and ' .
|
||||
urlencode($httpsroot . '/pluginfile.php/123') . '.'
|
||||
);
|
||||
$this->assertEquals('$@COURSEVIEWBYID*123@$, $@COURSEVIEWBYID*123@$, $@GRADEINDEXBYID*123@$, ' .
|
||||
'$@GRADEREPORTINDEXBYID*123@$, $@BADGESVIEWBYID*123@$ and $@USERINDEXVIEWBYID*123@$.', $encoded);
|
||||
'$@GRADEREPORTINDEXBYID*123@$, $@BADGESVIEWBYID*123@$, $@USERINDEXVIEWBYID*123@$, ' .
|
||||
'$@PLUGINFILEBYCONTEXT*123@$ and $@PLUGINFILEBYCONTEXTURLENCODED*123@$.', $encoded);
|
||||
|
||||
// HTTP root and links of both types in content.
|
||||
$CFG->wwwroot = $httproot;
|
||||
@@ -66,10 +70,14 @@ class backup_encode_content_test extends \basic_testcase {
|
||||
$httpsroot . '/course/view.php?id=123, ' .
|
||||
$httproot . '/grade/index.php?id=123, ' .
|
||||
$httproot . '/grade/report/index.php?id=123, ' .
|
||||
$httproot . '/badges/view.php?type=2&id=123 and ' .
|
||||
$httproot . '/user/index.php?id=123.');
|
||||
$httproot . '/badges/view.php?type=2&id=123, ' .
|
||||
$httproot . '/user/index.php?id=123, ' .
|
||||
$httproot . '/pluginfile.php/123 and ' .
|
||||
urlencode($httproot . '/pluginfile.php/123') . '.'
|
||||
);
|
||||
$this->assertEquals('$@COURSEVIEWBYID*123@$, $@COURSEVIEWBYID*123@$, $@GRADEINDEXBYID*123@$, ' .
|
||||
'$@GRADEREPORTINDEXBYID*123@$, $@BADGESVIEWBYID*123@$ and $@USERINDEXVIEWBYID*123@$.', $encoded);
|
||||
'$@GRADEREPORTINDEXBYID*123@$, $@BADGESVIEWBYID*123@$, $@USERINDEXVIEWBYID*123@$, ' .
|
||||
'$@PLUGINFILEBYCONTEXT*123@$ and $@PLUGINFILEBYCONTEXTURLENCODED*123@$.', $encoded);
|
||||
$CFG->wwwroot = $oldroot;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -34,8 +34,7 @@ Feature: Delete course badge already awarded
|
||||
# Navigate to Manage Badges page in order to delete the badge
|
||||
And I navigate to "Badges > Manage badges" in current page administration
|
||||
# Delete the badge
|
||||
And I open the action menu in "<badgename>" "table_row"
|
||||
And I choose "Delete" in the open action menu
|
||||
And I press "Delete" action in the "<badgename>" report row
|
||||
And I press "<deleteoption>"
|
||||
And I am on the "Course 1" "enrolled users" page
|
||||
And I click on "Student 1" "link"
|
||||
|
||||
@@ -54,16 +54,14 @@ Feature: Manage badges
|
||||
And I set the field "Manager" to "1"
|
||||
And I press "Save"
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
And I open the action menu in "Badge #1" "table_row"
|
||||
And I choose "Enable access" in the open action menu
|
||||
And I press "Enable access" action in the "Badge #1" report row
|
||||
And I should see "Changes in badge access"
|
||||
And I press "Continue"
|
||||
And I should see "Access to the badges was successfully enabled"
|
||||
Then the following should exist in the "reportbuilder-table" table:
|
||||
| Name | Badge status |
|
||||
| Badge #1 | Available |
|
||||
And I open the action menu in "Badge #1" "table_row"
|
||||
And I choose "Disable access" in the open action menu
|
||||
And I press "Disable access" action in the "Badge #1" report row
|
||||
And I should see "Access to the badges was successfully disabled"
|
||||
And the following should exist in the "reportbuilder-table" table:
|
||||
| Name | Badge status |
|
||||
@@ -78,14 +76,12 @@ Feature: Manage badges
|
||||
And I set the field "Manager" to "1"
|
||||
And I press "Save"
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
And I open the action menu in "Badge #1" "table_row"
|
||||
And I choose "Enable access" in the open action menu
|
||||
And I press "Enable access" action in the "Badge #1" report row
|
||||
And I press "Continue"
|
||||
And I open the action menu in "Badge #1" "table_row"
|
||||
And I choose "Award badge" in the open action menu
|
||||
And I press "Award badge" action in the "Badge #1" report row
|
||||
And I set the field "potentialrecipients[]" to "Admin User (moodle@example.com)"
|
||||
And I press "Award badge"
|
||||
And I navigate to "Badges > Manage badges" in site administration
|
||||
And the following should exist in the "reportbuilder-table" table:
|
||||
Then the following should exist in the "reportbuilder-table" table:
|
||||
| Name | Badge status | Recipients |
|
||||
| Badge #1 | Available | 1 |
|
||||
|
||||
@@ -10,8 +10,8 @@ Feature: View a site event on the dashboard
|
||||
| student1 | Student | 1 | student1@example.com | S1 |
|
||||
And I log in as "admin"
|
||||
And I create a calendar event with form data:
|
||||
| id_eventtype | Site |
|
||||
| id_name | Site Event |
|
||||
| id_eventtype | Site |
|
||||
| id_name | Site Event |
|
||||
And I log out
|
||||
|
||||
@javascript
|
||||
@@ -29,5 +29,5 @@ Feature: View a site event on the dashboard
|
||||
# We need to give the browser a couple seconds to re-render the page after the screen has been resized.
|
||||
And I wait "1" seconds
|
||||
And I should not see "Site Event"
|
||||
And I hover over today in the mini-calendar block
|
||||
When I hover over today in the mini-calendar block responsive view
|
||||
And I should see "Site Event"
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -547,7 +547,11 @@ const itemsPerPageFunc = (pagingLimit, root) => {
|
||||
// Filter out all pagination options which are too large for the amount of courses user is enrolled in.
|
||||
const totalCourseCount = parseInt(root.find(SELECTORS.courseView.region).attr('data-totalcoursecount'), 10);
|
||||
return itemsPerPage.filter(pagingOption => {
|
||||
return pagingOption.value < totalCourseCount || pagingOption.value === 0;
|
||||
if (pagingOption.value === 0 && totalCourseCount > 100) {
|
||||
// To minimise performance issues, do not show the "All" option if the user is enrolled in more than 100 courses.
|
||||
return false;
|
||||
}
|
||||
return pagingOption.value < totalCourseCount;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -363,7 +363,14 @@ class main implements renderable, templatable {
|
||||
if (!$this->displaygroupingcustomfield) {
|
||||
return [];
|
||||
}
|
||||
$fieldid = $DB->get_field('customfield_field', 'id', ['shortname' => $this->customfiltergrouping]);
|
||||
|
||||
// Get the relevant customfield ID within the core_course/course component/area.
|
||||
$fieldid = $DB->get_field_sql("
|
||||
SELECT f.id
|
||||
FROM {customfield_field} f
|
||||
JOIN {customfield_category} c ON c.id = f.categoryid
|
||||
WHERE f.shortname = :shortname AND c.component = 'core_course' AND c.area = 'course'
|
||||
", ['shortname' => $this->customfiltergrouping]);
|
||||
if (!$fieldid) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -51,8 +51,7 @@
|
||||
<div class="text-xs-center text-center mt-3" data-region="empty-message">
|
||||
<img class="empty-placeholder-image-lg mt-1"
|
||||
src="{{nocoursesimg}}"
|
||||
alt="{{{ title }}}"
|
||||
role="presentation"
|
||||
alt=""
|
||||
>
|
||||
<h5 class="h5 mt-3 mb-0">{{{ title }}}</h5>
|
||||
<p class="mt-3 mb-0">{{{ intro }}}</p>
|
||||
|
||||
@@ -70,3 +70,8 @@ Feature: Zero state on my overview block
|
||||
And "Create course" "button" should exist
|
||||
And I click on "Create course" "button"
|
||||
And I should see "Add a new course"
|
||||
|
||||
@accessibility
|
||||
Scenario: Evaluate the accessibility of the My courses (zero state)
|
||||
When I am on the "My courses" page logged in as "manager"
|
||||
Then the page should meet accessibility standards
|
||||
|
||||
@@ -27,9 +27,8 @@
|
||||
<div class="text-xs-center text-center mt-4" data-region="empty-message">
|
||||
<img
|
||||
src="{{noitemsimgurl}}"
|
||||
alt="{{#str}} noitems, block_recentlyaccesseditems {{/str}}"
|
||||
role="presentation"
|
||||
alt=""
|
||||
style="height: 70px; width: 70px;"
|
||||
>
|
||||
<p class="text-muted mt-3 mb-0">{{#str}} noitems, block_recentlyaccesseditems {{/str}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -42,19 +42,21 @@
|
||||
|
||||
<div class="card-deck dashboard-card-deck one-row" data-region="items-list" role="list">
|
||||
{{#items}}
|
||||
<a class="card dashboard-card mb-1" href="{{{viewurl}}}" title="{{{name}}}" role="listitem">
|
||||
<div class="card-body p-2 m-1">
|
||||
<div class="d-flex text-truncate">
|
||||
<div class="d-flex align-self-center activityiconcontainer {{purpose}}">
|
||||
{{{icon}}}
|
||||
</div>
|
||||
<div class="w-100 line-height-3 text-truncate ml-2">
|
||||
<h6 class="mb-0 text-truncate"><span class="clickable">{{{name}}}</span></h6>
|
||||
<small class="text-truncate mb-0">{{{coursename}}}</small>
|
||||
<div class="card dashboard-card" role="listitem">
|
||||
<a class="mb-1" href="{{{viewurl}}}" title="{{{name}}}">
|
||||
<div class="card-body p-2 m-1">
|
||||
<div class="d-flex text-truncate">
|
||||
<div class="d-flex align-self-center activityiconcontainer {{purpose}}">
|
||||
{{{icon}}}
|
||||
</div>
|
||||
<div class="w-100 line-height-3 text-truncate ml-2">
|
||||
<h6 class="mb-0 text-truncate"><span class="clickable">{{{name}}}</span></h6>
|
||||
<small class="text-truncate mb-0">{{{coursename}}}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
{{/items}}
|
||||
</div>
|
||||
{{#hasmoreitems}}
|
||||
|
||||
@@ -23,15 +23,19 @@ Feature: The recently accessed items block allows users to easily access their m
|
||||
| name | Test forum name |
|
||||
And I log in as "student1"
|
||||
|
||||
@accessibility
|
||||
Scenario: User has not accessed any item
|
||||
Then I should see "No recent items" in the "Recently accessed items" "block"
|
||||
And the page should meet accessibility standards
|
||||
|
||||
@accessibility
|
||||
Scenario: User has accessed some items
|
||||
Given I change window size to "large"
|
||||
When I am on the "Test forum name" "forum activity" page
|
||||
And I follow "Dashboard"
|
||||
Then I should see "Test forum name" in the "Recently accessed items" "block"
|
||||
And I should not see "Show more items" in the "Recently accessed items" "block"
|
||||
And the page should meet accessibility standards
|
||||
|
||||
Scenario: User has accessed more than 3 items
|
||||
Given the following "activities" exist:
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
@block_starredcourses
|
||||
Feature: Starred courses
|
||||
In order for me to quickly navigate to my favourite courses
|
||||
As a user
|
||||
I must be able to add them to the Starred courses block
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| student1 | Student | 1 | student1@example.com |
|
||||
And the following "courses" exist:
|
||||
| shortname | fullname |
|
||||
| C1 | Course 1 |
|
||||
| C2 | Course 2 |
|
||||
| C3 | Course 3 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| student1 | C1 | student |
|
||||
| student1 | C2 | student |
|
||||
| student1 | C3 | student |
|
||||
And the following "blocks" exist:
|
||||
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
|
||||
| starredcourses | User | student1 | my-index | content |
|
||||
|
||||
@accessibility @javascript
|
||||
Scenario: User has no starred courses
|
||||
Given I log in as "student1"
|
||||
Then I should see "No starred courses"
|
||||
And the page should meet accessibility standards
|
||||
|
||||
@accessibility @javascript
|
||||
Scenario: User has starred courses
|
||||
Given I am on the "My courses" page logged in as "student1"
|
||||
And I click on "Actions for course Course 1" "button"
|
||||
And I click on "Star for Course 1" "link"
|
||||
And I click on "Actions for course Course 3" "button"
|
||||
And I click on "Star for Course 3" "link"
|
||||
When I follow "Dashboard"
|
||||
Then the page should meet accessibility standards
|
||||
And I should see "Course 1" in the "Starred courses" "block"
|
||||
And I should see "Course 3" in the "Starred courses" "block"
|
||||
But I should not see "Course 2" in the "Starred courses" "block"
|
||||
@@ -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
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -6,6 +6,6 @@ define("core_calendar/popover",["theme_boost/popover","jquery","core_calendar/se
|
||||
* @copyright 2021 Huong Nguyen <huongnv13@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @since 4.0
|
||||
*/function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj},CalendarSelectors=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(CalendarSelectors);const isPopoverConfigured=new Map,showPopover=target=>{if(!isPopoverConfigured.has(target)){const dateEle=(0,_jquery.default)(target);dateEle.popover({trigger:"manual",placement:"top",html:!0,content:()=>{const source=dateEle.find(CalendarSelectors.elements.dateContent),content=(0,_jquery.default)("<div>");if(source.length){const temptContent=source.find(".hidden").clone(!1);content.html(temptContent.html())}return content.html()}}),isPopoverConfigured.set(target,!0)}var dateContainer;dateContainer=target,"none"===window.getComputedStyle(dateContainer.querySelector(CalendarSelectors.elements.dateContent)).display&&((0,_jquery.default)(target).popover("show"),target.addEventListener("mouseleave",hidePopover),target.addEventListener("focusout",hidePopover))},hidePopover=e=>{const target=e.target,dateContainer=e.target.closest(CalendarSelectors.elements.dateContainer);if(dateContainer&&isPopoverConfigured.has(dateContainer)){const isTargetActive=target.contains(document.activeElement),isTargetHover=target.matches(":hover");isTargetActive||isTargetHover||((0,_jquery.default)(dateContainer).popover("hide"),dateContainer.removeEventListener("mouseleave",hidePopover),dateContainer.removeEventListener("focusout",hidePopover))}};let listenersRegistered=!1;listenersRegistered||((()=>{const showPopoverHandler=e=>{const dateContainer=e.target.closest(CalendarSelectors.elements.dateContainer);dateContainer&&(e.preventDefault(),showPopover(dateContainer))};document.addEventListener("mouseover",showPopoverHandler),document.addEventListener("focusin",showPopoverHandler)})(),listenersRegistered=!0)}));
|
||||
*/function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj},CalendarSelectors=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(CalendarSelectors);const isPopoverConfigured=new Map,showPopover=target=>{const dateContainer=target.closest(CalendarSelectors.elements.dateContainer);if(!isPopoverConfigured.has(dateContainer)){(0,_jquery.default)(target).popover({trigger:"manual",placement:"top",html:!0,title:dateContainer.dataset.title,content:()=>{const source=(0,_jquery.default)(dateContainer).find(CalendarSelectors.elements.dateContent),content=(0,_jquery.default)("<div>");if(source.length){const temptContent=source.find(".hidden").clone(!1);content.html(temptContent.html())}return content.html()}}),isPopoverConfigured.set(dateContainer,!0)}(dateContainer=>"none"===window.getComputedStyle(dateContainer.querySelector(CalendarSelectors.elements.dateContent)).display)(dateContainer)&&((0,_jquery.default)(target).popover("show"),target.addEventListener("mouseleave",hidePopover),target.addEventListener("focusout",hidePopover))},hidePopover=e=>{const target=e.target,dateContainer=e.target.closest(CalendarSelectors.elements.dateContainer);if(dateContainer&&isPopoverConfigured.has(dateContainer)){const isTargetActive=target.contains(document.activeElement),isTargetHover=target.matches(":hover");isTargetActive||isTargetHover||((0,_jquery.default)(target).popover("hide"),target.removeEventListener("mouseleave",hidePopover),target.removeEventListener("focusout",hidePopover))}};let listenersRegistered=!1;listenersRegistered||((()=>{const showPopoverHandler=e=>{const dayLink=e.target.closest(CalendarSelectors.links.dayLink);dayLink&&(e.preventDefault(),showPopover(dayLink))};document.addEventListener("mouseover",showPopoverHandler),document.addEventListener("focusin",showPopoverHandler)})(),listenersRegistered=!0)}));
|
||||
|
||||
//# sourceMappingURL=popover.min.js.map
|
||||
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -5,6 +5,6 @@
|
||||
* @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define("core_calendar/selectors",[],(function(){return{eventFilterItem:"[data-action='filter-event-type']",eventType:{site:"[data-eventtype-site]",category:"[data-eventtype-category]",course:"[data-eventtype-course]",group:"[data-eventtype-group]",user:"[data-eventtype-user]",other:"[data-eventtype-other]"},popoverType:{site:"[data-popover-eventtype-site]",category:"[data-popover-eventtype-category]",course:"[data-popover-eventtype-course]",group:"[data-popover-eventtype-group]",user:"[data-popover-eventtype-user]",other:"[data-popover-eventtype-other]"},calendarPeriods:{month:"[data-period='month']"},courseSelector:'select[name="course"]',viewSelector:'div[data-region="view-selector"]',actions:{create:'[data-action="new-event-button"]',edit:'[data-action="edit"]',remove:'[data-action="delete"]',viewEvent:'[data-action="view-event"]',deleteSubscription:'[data-action="delete-subscription"]'},elements:{courseSelector:'select[name="course"]',dateContainer:".clickable.hasevent",dateContent:'[data-region="day-content"]',monthDetailed:".calendarmonth.calendartable"},today:".today",day:'[data-region="day"]',calendarMain:'[data-region="calendar"]',wrapper:".calendarwrapper",eventItem:'[data-type="event"]',links:{navLink:".calendarwrapper .arrow_link",eventLink:"[data-region='event-item']",miniDayLink:"[data-region='mini-day-link']"},containers:{loadingIcon:'[data-region="overlay-icon-container"]'},mainCalendar:".maincalendar .heightcontainer",fullCalendarView:"page-calendar-view"}}));
|
||||
define("core_calendar/selectors",[],(function(){return{eventFilterItem:"[data-action='filter-event-type']",eventType:{site:"[data-eventtype-site]",category:"[data-eventtype-category]",course:"[data-eventtype-course]",group:"[data-eventtype-group]",user:"[data-eventtype-user]",other:"[data-eventtype-other]"},popoverType:{site:"[data-popover-eventtype-site]",category:"[data-popover-eventtype-category]",course:"[data-popover-eventtype-course]",group:"[data-popover-eventtype-group]",user:"[data-popover-eventtype-user]",other:"[data-popover-eventtype-other]"},calendarPeriods:{month:"[data-period='month']"},courseSelector:'select[name="course"]',viewSelector:'div[data-region="view-selector"]',actions:{create:'[data-action="new-event-button"]',edit:'[data-action="edit"]',remove:'[data-action="delete"]',viewEvent:'[data-action="view-event"]',deleteSubscription:'[data-action="delete-subscription"]'},elements:{courseSelector:'select[name="course"]',dateContainer:".clickable.hasevent",dateContent:'[data-region="day-content"]',monthDetailed:".calendarmonth.calendartable"},today:".today",day:'[data-region="day"]',calendarMain:'[data-region="calendar"]',wrapper:".calendarwrapper",eventItem:'[data-type="event"]',links:{navLink:".calendarwrapper .arrow_link",eventLink:"[data-region='event-item']",miniDayLink:"[data-region='mini-day-link']",dayLink:"[data-action='view-day-link']"},containers:{loadingIcon:'[data-region="overlay-icon-container"]'},mainCalendar:".maincalendar .heightcontainer",fullCalendarView:"page-calendar-view"}}));
|
||||
|
||||
//# sourceMappingURL=selectors.min.js.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"selectors.min.js","sources":["../src/selectors.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * CSS selectors for the calendar.\n *\n * @module core_calendar/selectors\n * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([], function() {\n return {\n eventFilterItem: \"[data-action='filter-event-type']\",\n eventType: {\n site: \"[data-eventtype-site]\",\n category: \"[data-eventtype-category]\",\n course: \"[data-eventtype-course]\",\n group: \"[data-eventtype-group]\",\n user: \"[data-eventtype-user]\",\n other: \"[data-eventtype-other]\",\n },\n popoverType: {\n site: \"[data-popover-eventtype-site]\",\n category: \"[data-popover-eventtype-category]\",\n course: \"[data-popover-eventtype-course]\",\n group: \"[data-popover-eventtype-group]\",\n user: \"[data-popover-eventtype-user]\",\n other: \"[data-popover-eventtype-other]\",\n },\n calendarPeriods: {\n month: \"[data-period='month']\",\n },\n courseSelector: 'select[name=\"course\"]',\n viewSelector: 'div[data-region=\"view-selector\"]',\n actions: {\n create: '[data-action=\"new-event-button\"]',\n edit: '[data-action=\"edit\"]',\n remove: '[data-action=\"delete\"]',\n viewEvent: '[data-action=\"view-event\"]',\n deleteSubscription: '[data-action=\"delete-subscription\"]',\n },\n elements: {\n courseSelector: 'select[name=\"course\"]',\n dateContainer: '.clickable.hasevent',\n dateContent: '[data-region=\"day-content\"]',\n monthDetailed: '.calendarmonth.calendartable',\n },\n today: '.today',\n day: '[data-region=\"day\"]',\n calendarMain: '[data-region=\"calendar\"]',\n wrapper: '.calendarwrapper',\n eventItem: '[data-type=\"event\"]',\n links: {\n navLink: '.calendarwrapper .arrow_link',\n eventLink: \"[data-region='event-item']\",\n miniDayLink: \"[data-region='mini-day-link']\",\n },\n containers: {\n loadingIcon: '[data-region=\"overlay-icon-container\"]',\n },\n mainCalendar: '.maincalendar .heightcontainer',\n fullCalendarView: 'page-calendar-view',\n };\n});\n"],"names":["define","eventFilterItem","eventType","site","category","course","group","user","other","popoverType","calendarPeriods","month","courseSelector","viewSelector","actions","create","edit","remove","viewEvent","deleteSubscription","elements","dateContainer","dateContent","monthDetailed","today","day","calendarMain","wrapper","eventItem","links","navLink","eventLink","miniDayLink","containers","loadingIcon","mainCalendar","fullCalendarView"],"mappings":";;;;;;;AAsBAA,iCAAO,IAAI,iBACA,CACHC,gBAAiB,oCACjBC,UAAW,CACPC,KAAM,wBACNC,SAAU,4BACVC,OAAQ,0BACRC,MAAO,yBACPC,KAAM,wBACNC,MAAO,0BAEXC,YAAa,CACTN,KAAM,gCACNC,SAAU,oCACVC,OAAQ,kCACRC,MAAO,iCACPC,KAAM,gCACNC,MAAO,kCAEXE,gBAAiB,CACbC,MAAO,yBAEXC,eAAgB,wBAChBC,aAAc,mCACdC,QAAS,CACLC,OAAQ,mCACRC,KAAM,uBACNC,OAAQ,yBACRC,UAAW,6BACXC,mBAAoB,uCAExBC,SAAU,CACNR,eAAgB,wBAChBS,cAAe,sBACfC,YAAa,8BACbC,cAAe,gCAEnBC,MAAO,SACPC,IAAK,sBACLC,aAAc,2BACdC,QAAS,mBACTC,UAAW,sBACXC,MAAO,CACHC,QAAS,+BACTC,UAAW,6BACXC,YAAa,iCAEjBC,WAAY,CACRC,YAAa,0CAEjBC,aAAc,iCACdC,iBAAkB"}
|
||||
{"version":3,"file":"selectors.min.js","sources":["../src/selectors.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * CSS selectors for the calendar.\n *\n * @module core_calendar/selectors\n * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine([], function() {\n return {\n eventFilterItem: \"[data-action='filter-event-type']\",\n eventType: {\n site: \"[data-eventtype-site]\",\n category: \"[data-eventtype-category]\",\n course: \"[data-eventtype-course]\",\n group: \"[data-eventtype-group]\",\n user: \"[data-eventtype-user]\",\n other: \"[data-eventtype-other]\",\n },\n popoverType: {\n site: \"[data-popover-eventtype-site]\",\n category: \"[data-popover-eventtype-category]\",\n course: \"[data-popover-eventtype-course]\",\n group: \"[data-popover-eventtype-group]\",\n user: \"[data-popover-eventtype-user]\",\n other: \"[data-popover-eventtype-other]\",\n },\n calendarPeriods: {\n month: \"[data-period='month']\",\n },\n courseSelector: 'select[name=\"course\"]',\n viewSelector: 'div[data-region=\"view-selector\"]',\n actions: {\n create: '[data-action=\"new-event-button\"]',\n edit: '[data-action=\"edit\"]',\n remove: '[data-action=\"delete\"]',\n viewEvent: '[data-action=\"view-event\"]',\n deleteSubscription: '[data-action=\"delete-subscription\"]',\n },\n elements: {\n courseSelector: 'select[name=\"course\"]',\n dateContainer: '.clickable.hasevent',\n dateContent: '[data-region=\"day-content\"]',\n monthDetailed: '.calendarmonth.calendartable',\n },\n today: '.today',\n day: '[data-region=\"day\"]',\n calendarMain: '[data-region=\"calendar\"]',\n wrapper: '.calendarwrapper',\n eventItem: '[data-type=\"event\"]',\n links: {\n navLink: '.calendarwrapper .arrow_link',\n eventLink: \"[data-region='event-item']\",\n miniDayLink: \"[data-region='mini-day-link']\",\n dayLink: \"[data-action='view-day-link']\",\n },\n containers: {\n loadingIcon: '[data-region=\"overlay-icon-container\"]',\n },\n mainCalendar: '.maincalendar .heightcontainer',\n fullCalendarView: 'page-calendar-view',\n };\n});\n"],"names":["define","eventFilterItem","eventType","site","category","course","group","user","other","popoverType","calendarPeriods","month","courseSelector","viewSelector","actions","create","edit","remove","viewEvent","deleteSubscription","elements","dateContainer","dateContent","monthDetailed","today","day","calendarMain","wrapper","eventItem","links","navLink","eventLink","miniDayLink","dayLink","containers","loadingIcon","mainCalendar","fullCalendarView"],"mappings":";;;;;;;AAsBAA,iCAAO,IAAI,iBACA,CACHC,gBAAiB,oCACjBC,UAAW,CACPC,KAAM,wBACNC,SAAU,4BACVC,OAAQ,0BACRC,MAAO,yBACPC,KAAM,wBACNC,MAAO,0BAEXC,YAAa,CACTN,KAAM,gCACNC,SAAU,oCACVC,OAAQ,kCACRC,MAAO,iCACPC,KAAM,gCACNC,MAAO,kCAEXE,gBAAiB,CACbC,MAAO,yBAEXC,eAAgB,wBAChBC,aAAc,mCACdC,QAAS,CACLC,OAAQ,mCACRC,KAAM,uBACNC,OAAQ,yBACRC,UAAW,6BACXC,mBAAoB,uCAExBC,SAAU,CACNR,eAAgB,wBAChBS,cAAe,sBACfC,YAAa,8BACbC,cAAe,gCAEnBC,MAAO,SACPC,IAAK,sBACLC,aAAc,2BACdC,QAAS,mBACTC,UAAW,sBACXC,MAAO,CACHC,QAAS,+BACTC,UAAW,6BACXC,YAAa,gCACbC,QAAS,iCAEbC,WAAY,CACRC,YAAa,0CAEjBC,aAAc,iCACdC,iBAAkB"}
|
||||
@@ -34,6 +34,7 @@ define([
|
||||
'core_calendar/crud',
|
||||
'core_calendar/selectors',
|
||||
'core/config',
|
||||
'core/str',
|
||||
],
|
||||
function(
|
||||
$,
|
||||
@@ -45,6 +46,7 @@ function(
|
||||
CalendarCrud,
|
||||
CalendarSelectors,
|
||||
Config,
|
||||
Str,
|
||||
) {
|
||||
|
||||
var SELECTORS = {
|
||||
@@ -57,7 +59,9 @@ function(
|
||||
CALENDAR_MONTH_WRAPPER: ".calendarwrapper",
|
||||
TODAY: '.today',
|
||||
DAY_NUMBER_CIRCLE: '.day-number-circle',
|
||||
DAY_NUMBER: '.day-number'
|
||||
DAY_NUMBER: '.day-number',
|
||||
SCREEN_READER_ANNOUNCEMENTS: '.calendar-announcements',
|
||||
CURRENT_MONTH: '.calendar-controls .current'
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -151,6 +155,12 @@ function(
|
||||
body.on(CalendarEvents.eventMoved, function() {
|
||||
CalendarViewManager.reloadCurrentMonth(root);
|
||||
});
|
||||
// Announce the newly loaded month to screen readers.
|
||||
body.on(CalendarEvents.monthChanged, root, async function() {
|
||||
const monthName = body.find(SELECTORS.CURRENT_MONTH).text();
|
||||
const monthAnnoucement = await Str.get_string('newmonthannouncement', 'calendar', monthName);
|
||||
body.find(SELECTORS.SCREEN_READER_ANNOUNCEMENTS).html(monthAnnoucement);
|
||||
});
|
||||
|
||||
CalendarCrud.registerEditListeners(root, eventFormModalPromise);
|
||||
};
|
||||
|
||||
+12
-10
@@ -38,14 +38,16 @@ const isPopoverAvailable = (dateContainer) => {
|
||||
const isPopoverConfigured = new Map();
|
||||
|
||||
const showPopover = target => {
|
||||
if (!isPopoverConfigured.has(target)) {
|
||||
const dateContainer = target.closest(CalendarSelectors.elements.dateContainer);
|
||||
if (!isPopoverConfigured.has(dateContainer)) {
|
||||
const dateEle = jQuery(target);
|
||||
dateEle.popover({
|
||||
trigger: 'manual',
|
||||
placement: 'top',
|
||||
html: true,
|
||||
title: dateContainer.dataset.title,
|
||||
content: () => {
|
||||
const source = dateEle.find(CalendarSelectors.elements.dateContent);
|
||||
const source = jQuery(dateContainer).find(CalendarSelectors.elements.dateContent);
|
||||
const content = jQuery('<div>');
|
||||
if (source.length) {
|
||||
const temptContent = source.find('.hidden').clone(false);
|
||||
@@ -55,10 +57,10 @@ const showPopover = target => {
|
||||
}
|
||||
});
|
||||
|
||||
isPopoverConfigured.set(target, true);
|
||||
isPopoverConfigured.set(dateContainer, true);
|
||||
}
|
||||
|
||||
if (isPopoverAvailable(target)) {
|
||||
if (isPopoverAvailable(dateContainer)) {
|
||||
jQuery(target).popover('show');
|
||||
target.addEventListener('mouseleave', hidePopover);
|
||||
target.addEventListener('focusout', hidePopover);
|
||||
@@ -75,9 +77,9 @@ const hidePopover = e => {
|
||||
const isTargetActive = target.contains(document.activeElement);
|
||||
const isTargetHover = target.matches(':hover');
|
||||
if (!isTargetActive && !isTargetHover) {
|
||||
jQuery(dateContainer).popover('hide');
|
||||
dateContainer.removeEventListener('mouseleave', hidePopover);
|
||||
dateContainer.removeEventListener('focusout', hidePopover);
|
||||
jQuery(target).popover('hide');
|
||||
target.removeEventListener('mouseleave', hidePopover);
|
||||
target.removeEventListener('focusout', hidePopover);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -87,13 +89,13 @@ const hidePopover = e => {
|
||||
*/
|
||||
const registerEventListeners = () => {
|
||||
const showPopoverHandler = (e) => {
|
||||
const dateContainer = e.target.closest(CalendarSelectors.elements.dateContainer);
|
||||
if (!dateContainer) {
|
||||
const dayLink = e.target.closest(CalendarSelectors.links.dayLink);
|
||||
if (!dayLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
showPopover(dateContainer);
|
||||
showPopover(dayLink);
|
||||
};
|
||||
|
||||
document.addEventListener('mouseover', showPopoverHandler);
|
||||
|
||||
@@ -66,6 +66,7 @@ define([], function() {
|
||||
navLink: '.calendarwrapper .arrow_link',
|
||||
eventLink: "[data-region='event-item']",
|
||||
miniDayLink: "[data-region='mini-day-link']",
|
||||
dayLink: "[data-action='view-day-link']",
|
||||
},
|
||||
containers: {
|
||||
loadingIcon: '[data-region="overlay-icon-container"]',
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -118,9 +118,9 @@ class provider implements
|
||||
(e.courseid = ctx.instanceid AND e.eventtype = 'course' AND ctx.contextlevel = :coursecontext) OR
|
||||
(e.courseid = ctx.instanceid AND e.eventtype = 'group' AND ctx.contextlevel = :groupcontext) OR
|
||||
(e.userid = ctx.instanceid AND e.eventtype = 'user' AND ctx.contextlevel = :usercontext)
|
||||
WHERE e.userid = :cuserid
|
||||
UNION
|
||||
SELECT ctx.id
|
||||
WHERE e.userid = :cuserid";
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
$sql = "SELECT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {course_modules} cm ON cm.id = ctx.instanceid AND ctx.contextlevel = :modulecontext
|
||||
JOIN {modules} m ON m.id = cm.module
|
||||
|
||||
@@ -39,12 +39,9 @@ if (empty($CFG->enablecalendarexport)) {
|
||||
die('no export');
|
||||
}
|
||||
|
||||
// Fetch basic user information to correctly log the user.
|
||||
$fields = 'id,username,password,firstname,lastname';
|
||||
|
||||
$checkuserid = !empty($userid) && $user = $DB->get_record('user', array('id' => $userid), $fields);
|
||||
$checkuserid = !empty($userid) && $user = \core_user::get_user($userid);
|
||||
// Allowing for fallback check of old url - MDL-27542.
|
||||
$checkusername = !empty($username) && $user = $DB->get_record('user', array('username' => $username), $fields);
|
||||
$checkusername = !empty($username) && $user = \core_user::get_user_by_username($username);
|
||||
if ((!$checkuserid && !$checkusername) || !$user) {
|
||||
//No such user
|
||||
die('Invalid authentication');
|
||||
|
||||
@@ -902,7 +902,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);
|
||||
|
||||
@@ -39,4 +39,5 @@
|
||||
{{{filter_selector}}}
|
||||
{{/filter_selector}}
|
||||
{{> core_calendar/add_event_button}}
|
||||
</div>
|
||||
</div>
|
||||
<div aria-live="polite" class="sr-only calendar-announcements"></div>
|
||||
|
||||
@@ -48,6 +48,10 @@ class behat_calendar extends behat_base {
|
||||
new behat_component_named_selector('mini calendar block', [".//*[@data-block='calendar_month']"]),
|
||||
new behat_component_named_selector('full calendar page', [".//*[@id='page-calendar-view']"]),
|
||||
new behat_component_named_selector('calendar day', [".//*[@data-region='day'][@data-day=%locator%]"]),
|
||||
new behat_component_named_selector(
|
||||
'responsive calendar day',
|
||||
[".//*[@data-region='day'][@data-day=%locator%]/div[contains(@class, 'hidden-desktop')]"]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -93,33 +97,51 @@ class behat_calendar extends behat_base {
|
||||
/**
|
||||
* Hover over a specific day in the mini-calendar.
|
||||
*
|
||||
* @Given /^I hover over day "(?P<dayofmonth>\d+)" of this month in the mini-calendar block$/
|
||||
* @Given /^I hover over day "(?P<dayofmonth>\d+)" of this month in the mini-calendar block(?P<responsive> responsive view|)$/
|
||||
* @param int $day The day of the current month
|
||||
* @param string $responsive If not null, find the responsive version of the link.
|
||||
*/
|
||||
public function i_hover_over_day_of_this_month_in_mini_calendar_block(int $day): void {
|
||||
$this->execute("behat_general::i_hover_in_the",
|
||||
[$day, 'core_calendar > calendar day', '', 'core_calendar > mini calendar block']);
|
||||
public function i_hover_over_day_of_this_month_in_mini_calendar_block(int $day, string $responsive = ''): void {
|
||||
$this->execute(
|
||||
"behat_general::i_hover_in_the",
|
||||
[
|
||||
$day,
|
||||
empty($responsive) ? 'core_calendar > calendar day' : 'core_calendar > responsive calendar day',
|
||||
'',
|
||||
'core_calendar > mini calendar block',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hover over a specific day in the full calendar page.
|
||||
*
|
||||
* @Given /^I hover over day "(?P<dayofmonth>\d+)" of this month in the full calendar page$/
|
||||
* @Given /^I hover over day "(?P<dayofmonth>\d+)" of this month in the full calendar page(?P<responsive> responsive view|)$/
|
||||
* @param int $day The day of the current month
|
||||
* @param string $responsive If not empty, use the repsonsive view.
|
||||
*/
|
||||
public function i_hover_over_day_of_this_month_in_full_calendar_page(int $day): void {
|
||||
$this->execute("behat_general::i_hover_in_the",
|
||||
[$day, 'core_calendar > calendar day', '', 'core_calendar > full calendar page']);
|
||||
public function i_hover_over_day_of_this_month_in_full_calendar_page(int $day, string $responsive = ''): void {
|
||||
$this->execute(
|
||||
"behat_general::i_hover_in_the",
|
||||
[
|
||||
$day,
|
||||
empty($responsive) ? 'core_calendar > calendar day' : 'core_calendar > responsive calendar day',
|
||||
'',
|
||||
'core_calendar > full calendar page',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hover over today in the mini-calendar.
|
||||
*
|
||||
* @Given /^I hover over today in the mini-calendar block$/
|
||||
* @Given /^I hover over today in the mini-calendar block( responsive view|)$/
|
||||
*
|
||||
* @param string $responsive If not empty, use the responsive calendar link.
|
||||
*/
|
||||
public function i_hover_over_today_in_mini_calendar_block(): void {
|
||||
public function i_hover_over_today_in_mini_calendar_block(string $responsive = ''): void {
|
||||
$todaysday = date('j');
|
||||
$this->i_hover_over_day_of_this_month_in_mini_calendar_block($todaysday);
|
||||
$this->i_hover_over_day_of_this_month_in_mini_calendar_block($todaysday, $responsive);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -278,7 +278,7 @@ Feature: Perform basic calendar functionality
|
||||
# We need to give the browser a couple seconds to re-render the page after the screen has been resized.
|
||||
And I wait "1" seconds
|
||||
And I should not see "Event 1:1"
|
||||
And I hover over day "1" of this month in the full calendar page
|
||||
And I hover over day "1" of this month in the full calendar page responsive view
|
||||
And I should see "Event 1:1"
|
||||
|
||||
@javascript
|
||||
|
||||
@@ -42,6 +42,7 @@ use core_privacy\local\request\approved_userlist;
|
||||
*
|
||||
* @copyright 2018 Zig Tan <zig@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \core_calendar\privacy\provider
|
||||
*/
|
||||
class provider_test extends provider_testcase {
|
||||
|
||||
|
||||
@@ -402,7 +402,7 @@ class core_cohort_external extends external_api {
|
||||
$results = cohort_get_cohorts($context->id, $limitfrom, $limitnum, $query);
|
||||
$results = $results['cohorts'];
|
||||
if (!$context instanceof context_system) {
|
||||
$results = array_merge($results, cohort_get_available_cohorts($context, COHORT_ALL, $limitfrom, $limitnum, $query));
|
||||
$results = $results + cohort_get_available_cohorts($context, COHORT_ALL, $limitfrom, $limitnum, $query);
|
||||
}
|
||||
} else if ($includes == 'all') {
|
||||
$results = cohort_get_all_cohorts($limitfrom, $limitnum, $query);
|
||||
@@ -414,7 +414,7 @@ class core_cohort_external extends external_api {
|
||||
$cohorts = array();
|
||||
|
||||
if (!empty($results)) {
|
||||
$cohortids = array_keys($results);
|
||||
$cohortids = array_column($results, 'id');
|
||||
$customfieldsdata = self::get_custom_fields_data($cohortids);
|
||||
}
|
||||
|
||||
|
||||
@@ -716,11 +716,16 @@ class externallib_test extends externallib_advanced_testcase {
|
||||
$this->assertCount(1, $result['cohorts']);
|
||||
$this->assertEquals('Cohortsearch 4', $result['cohorts'][$cohort4->id]->name);
|
||||
|
||||
$result = core_cohort_external::search_cohorts("Cohortsearch 4", $syscontext, 'parents');
|
||||
// A user with permissions in the system, searching category context.
|
||||
$result = core_cohort_external::search_cohorts("Cohortsearch 4", $catcontext, 'parents');
|
||||
$this->assertCount(1, $result['cohorts']);
|
||||
$this->assertEquals('Cohortsearch 4', $result['cohorts'][$cohort4->id]->name);
|
||||
$this->assertIsArray($result['cohorts'][$cohort4->id]->customfields);
|
||||
$this->assertCount(2, $result['cohorts'][$cohort4->id]->customfields);
|
||||
|
||||
$this->assertEqualsCanonicalizing([
|
||||
'Test value 1',
|
||||
'Test value 2',
|
||||
], array_column($result['cohorts'][$cohort4->id]->customfields, 'value'));
|
||||
|
||||
$actual = [];
|
||||
foreach ($result['cohorts'][$cohort4->id]->customfields as $customfield) {
|
||||
$this->assertArrayHasKey('name', $customfield);
|
||||
@@ -730,8 +735,6 @@ class externallib_test extends externallib_advanced_testcase {
|
||||
$this->assertArrayHasKey('value', $customfield);
|
||||
$actual[$customfield['shortname']] = $customfield;
|
||||
}
|
||||
$this->assertEquals('Test value 1', $actual['testfield1']['value']);
|
||||
$this->assertEquals('Test value 2', $actual['testfield2']['value']);
|
||||
|
||||
// A user with permissions in the category.
|
||||
$this->setUser($catcreator);
|
||||
|
||||
@@ -223,16 +223,28 @@ class provider implements
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . competency_framework::TABLE . "} cf
|
||||
ON cf.contextid = ctx.id
|
||||
LEFT JOIN {" . competency::TABLE . "} c
|
||||
ON c.competencyframeworkid = cf.id
|
||||
LEFT JOIN {" . related_competency::TABLE . "} cr
|
||||
ON cr.competencyid = c.id
|
||||
WHERE cf.usermodified = :userid1
|
||||
OR c.usermodified = :userid2
|
||||
OR cr.usermodified = :userid3";
|
||||
$params = ['userid1' => $userid, 'userid2' => $userid, 'userid3' => $userid];
|
||||
JOIN {" . competency_framework::TABLE . "} cf ON cf.contextid = ctx.id
|
||||
WHERE cf.usermodified = :userid1";
|
||||
$params = ['userid1' => $userid];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . competency_framework::TABLE . "} cf ON cf.contextid = ctx.id
|
||||
JOIN {" . competency::TABLE . "} c ON c.competencyframeworkid = cf.id
|
||||
WHERE c.usermodified = :userid2";
|
||||
$params = ['userid2' => $userid];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . competency_framework::TABLE . "} cf ON cf.contextid = ctx.id
|
||||
JOIN {" . competency::TABLE . "} c ON c.competencyframeworkid = cf.id
|
||||
JOIN {" . related_competency::TABLE . "} cr ON cr.competencyid = c.id
|
||||
WHERE cr.usermodified = :userid3";
|
||||
$params = ['userid3' => $userid];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
// Find the contexts of the templates, and related data, modified by the user.
|
||||
@@ -241,16 +253,30 @@ class provider implements
|
||||
FROM {context} ctx
|
||||
JOIN {" . template::TABLE . "} tpl
|
||||
ON tpl.contextid = ctx.id
|
||||
LEFT JOIN {" . template_cohort::TABLE . "} tch
|
||||
WHERE tpl.usermodified = :userid1";
|
||||
$params = ['userid1' => $userid];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . template::TABLE . "} tpl
|
||||
ON tpl.contextid = ctx.id
|
||||
JOIN {" . template_cohort::TABLE . "} tch
|
||||
ON tch.templateid = tpl.id
|
||||
AND tch.usermodified = :userid2
|
||||
LEFT JOIN {" . template_competency::TABLE . "} tc
|
||||
AND tch.usermodified = :userid2";
|
||||
$params = ['userid2' => $userid];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . template::TABLE . "} tpl
|
||||
ON tpl.contextid = ctx.id
|
||||
JOIN {" . template_competency::TABLE . "} tc
|
||||
ON tc.templateid = tpl.id
|
||||
AND tc.usermodified = :userid3
|
||||
WHERE tpl.usermodified = :userid1
|
||||
OR tch.id IS NOT NULL
|
||||
OR tc.id IS NOT NULL";
|
||||
$params = ['userid1' => $userid, 'userid2' => $userid, 'userid3' => $userid];
|
||||
AND tc.usermodified = :userid3";
|
||||
$params = ['userid3' => $userid];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
// Find the possible course contexts.
|
||||
@@ -302,21 +328,52 @@ class provider implements
|
||||
JOIN {context} ctx
|
||||
ON ctx.instanceid = p.userid
|
||||
AND ctx.contextlevel = :userlevel
|
||||
LEFT JOIN {" . plan_competency::TABLE . "} pc
|
||||
ON pc.planid = p.id
|
||||
AND pc.usermodified = :userid3
|
||||
LEFT JOIN {" . user_competency_plan::TABLE . "} upc
|
||||
ON upc.planid = p.id
|
||||
AND upc.usermodified = :userid4
|
||||
WHERE p.usermodified = :userid1
|
||||
OR p.reviewerid = :userid2
|
||||
OR pc.id IS NOT NULL
|
||||
OR upc.id IS NOT NULL";
|
||||
WHERE p.usermodified = :userid1";
|
||||
$params = [
|
||||
'userlevel' => CONTEXT_USER,
|
||||
'userid1' => $userid,
|
||||
];
|
||||
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {" . plan::TABLE . "} p
|
||||
JOIN {context} ctx
|
||||
ON ctx.instanceid = p.userid
|
||||
AND ctx.contextlevel = :userlevel
|
||||
WHERE p.reviewerid = :userid2";
|
||||
$params = [
|
||||
'userlevel' => CONTEXT_USER,
|
||||
'userid2' => $userid,
|
||||
];
|
||||
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {" . plan::TABLE . "} p
|
||||
JOIN {context} ctx
|
||||
ON ctx.instanceid = p.userid
|
||||
AND ctx.contextlevel = :userlevel
|
||||
JOIN {" . plan_competency::TABLE . "} pc
|
||||
ON pc.planid = p.id
|
||||
AND pc.usermodified = :userid3";
|
||||
$params = [
|
||||
'userlevel' => CONTEXT_USER,
|
||||
'userid3' => $userid,
|
||||
];
|
||||
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {" . plan::TABLE . "} p
|
||||
JOIN {context} ctx
|
||||
ON ctx.instanceid = p.userid
|
||||
AND ctx.contextlevel = :userlevel
|
||||
JOIN {" . user_competency_plan::TABLE . "} upc
|
||||
ON upc.planid = p.id
|
||||
AND upc.usermodified = :userid4";
|
||||
$params = [
|
||||
'userlevel' => CONTEXT_USER,
|
||||
'userid4' => $userid,
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
@@ -325,32 +382,78 @@ class provider implements
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
LEFT JOIN {" . user_competency::TABLE . "} uc
|
||||
JOIN {" . user_competency::TABLE . "} uc
|
||||
ON uc.userid = ctx.instanceid
|
||||
AND ctx.contextlevel = :userlevel1
|
||||
LEFT JOIN {" . evidence::TABLE . "} e
|
||||
ON e.usercompetencyid = uc.id
|
||||
AND (e.usermodified = :userid3 OR e.actionuserid = :userid4)
|
||||
LEFT JOIN {" . user_evidence::TABLE . "} ue
|
||||
ON ue.userid = ctx.instanceid
|
||||
AND ctx.contextlevel = :userlevel2
|
||||
AND ue.usermodified = :userid5
|
||||
LEFT JOIN {" . user_evidence_competency::TABLE . "} uec
|
||||
ON uec.userevidenceid = ue.id
|
||||
AND uec.usermodified = :userid6
|
||||
WHERE uc.usermodified = :userid1
|
||||
OR uc.reviewerid = :userid2
|
||||
OR e.id IS NOT NULL
|
||||
OR ue.id IS NOT NULL
|
||||
OR uec.id IS NOT NULL";
|
||||
WHERE uc.usermodified = :userid1";
|
||||
$params = [
|
||||
'userlevel1' => CONTEXT_USER,
|
||||
'userlevel2' => CONTEXT_USER,
|
||||
'userid1' => $userid,
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . user_competency::TABLE . "} uc
|
||||
ON uc.userid = ctx.instanceid
|
||||
AND ctx.contextlevel = :userlevel1
|
||||
WHERE uc.reviewerid = :userid2";
|
||||
$params = [
|
||||
'userlevel1' => CONTEXT_USER,
|
||||
'userid2' => $userid,
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . user_competency::TABLE . "} uc
|
||||
ON uc.userid = ctx.instanceid
|
||||
AND ctx.contextlevel = :userlevel1
|
||||
JOIN {" . evidence::TABLE . "} e
|
||||
ON e.usercompetencyid = uc.id
|
||||
AND e.usermodified = :userid3";
|
||||
$params = [
|
||||
'userlevel1' => CONTEXT_USER,
|
||||
'userid3' => $userid,
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . user_competency::TABLE . "} uc
|
||||
ON uc.userid = ctx.instanceid
|
||||
AND ctx.contextlevel = :userlevel1
|
||||
JOIN {" . evidence::TABLE . "} e
|
||||
ON e.usercompetencyid = uc.id
|
||||
AND e.actionuserid = :userid4";
|
||||
$params = [
|
||||
'userlevel1' => CONTEXT_USER,
|
||||
'userid4' => $userid,
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . user_evidence::TABLE . "} ue
|
||||
ON ue.userid = ctx.instanceid
|
||||
AND ctx.contextlevel = :userlevel2
|
||||
AND ue.usermodified = :userid5";
|
||||
$params = [
|
||||
'userlevel2' => CONTEXT_USER,
|
||||
'userid5' => $userid,
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . user_evidence::TABLE . "} ue
|
||||
ON ue.userid = ctx.instanceid
|
||||
AND ctx.contextlevel = :userlevel2
|
||||
JOIN {" . user_evidence_competency::TABLE . "} uec
|
||||
ON uec.userevidenceid = ue.id
|
||||
AND uec.usermodified = :userid6";
|
||||
$params = [
|
||||
'userlevel2' => CONTEXT_USER,
|
||||
'userid6' => $userid,
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
@@ -360,33 +463,48 @@ class provider implements
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
LEFT JOIN {" . plan::TABLE . "} p
|
||||
JOIN {" . plan::TABLE . "} p
|
||||
ON p.userid = ctx.instanceid
|
||||
AND ctx.contextlevel = :userlevel1
|
||||
LEFT JOIN {" . user_competency::TABLE . "} uc
|
||||
ON uc.userid = ctx.instanceid
|
||||
AND ctx.contextlevel = :userlevel2
|
||||
AND uc.userid = :userid2
|
||||
LEFT JOIN {" . user_evidence::TABLE . "} ue
|
||||
ON ue.userid = ctx.instanceid
|
||||
AND ctx.contextlevel = :userlevel3
|
||||
AND ue.userid = :userid3
|
||||
LEFT JOIN {" . user_competency_course::TABLE . "} ucc
|
||||
ON ucc.courseid = ctx.instanceid
|
||||
AND ctx.contextlevel = :courselevel
|
||||
AND ucc.userid = :userid4
|
||||
WHERE p.userid = :userid1
|
||||
OR uc.id IS NOT NULL
|
||||
OR ue.id IS NOT NULL
|
||||
OR ucc.id IS NOT NULL";
|
||||
WHERE p.userid = :userid1";
|
||||
$params = [
|
||||
'userlevel1' => CONTEXT_USER,
|
||||
'userlevel2' => CONTEXT_USER,
|
||||
'userlevel3' => CONTEXT_USER,
|
||||
'courselevel' => CONTEXT_COURSE,
|
||||
'userid1' => $userid,
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . user_competency::TABLE . "} uc
|
||||
ON uc.userid = ctx.instanceid
|
||||
AND ctx.contextlevel = :userlevel2
|
||||
AND uc.userid = :userid2";
|
||||
$params = [
|
||||
'userlevel2' => CONTEXT_USER,
|
||||
'userid2' => $userid,
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . user_evidence::TABLE . "} ue
|
||||
ON ue.userid = ctx.instanceid
|
||||
AND ctx.contextlevel = :userlevel3
|
||||
AND ue.userid = :userid3";
|
||||
$params = [
|
||||
'userlevel3' => CONTEXT_USER,
|
||||
'userid3' => $userid,
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
$sql = "
|
||||
SELECT DISTINCT ctx.id
|
||||
FROM {context} ctx
|
||||
JOIN {" . user_competency_course::TABLE . "} ucc
|
||||
ON ucc.courseid = ctx.instanceid
|
||||
AND ctx.contextlevel = :courselevel
|
||||
AND ucc.userid = :userid4";
|
||||
$params = [
|
||||
'courselevel' => CONTEXT_COURSE,
|
||||
'userid4' => $userid,
|
||||
];
|
||||
$contextlist->add_from_sql($sql, $params);
|
||||
|
||||
@@ -46,6 +46,7 @@ use core_competency\privacy\provider;
|
||||
* @copyright 2018 Frédéric Massart
|
||||
* @author Frédéric Massart <fred@branchup.tech>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \core_competency\privacy\provider
|
||||
*/
|
||||
class provider_test extends provider_testcase {
|
||||
|
||||
@@ -666,6 +667,7 @@ class provider_test extends provider_testcase {
|
||||
$u2 = $dg->create_user();
|
||||
$u3 = $dg->create_user();
|
||||
$u4 = $dg->create_user();
|
||||
$u5 = $dg->create_user();
|
||||
|
||||
$c1ctx = \context_course::instance($c1->id);
|
||||
$u1ctx = \context_user::instance($u1->id);
|
||||
@@ -681,30 +683,45 @@ class provider_test extends provider_testcase {
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u2->id), []);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u3->id), []);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u4->id), []);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u5->id), []);
|
||||
|
||||
$ccg->create_plan(['userid' => $u1->id]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u1->id), [$u1ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u2->id), []);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u3->id), []);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u4->id), []);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u5->id), []);
|
||||
|
||||
$ccg->create_user_competency(['userid' => $u2->id, 'competencyid' => $comp1->get('id')]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u1->id), [$u1ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u2->id), [$u2ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u3->id), []);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u4->id), []);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u5->id), []);
|
||||
|
||||
$ccg->create_user_competency_course(['userid' => $u3->id, 'competencyid' => $comp1->get('id'), 'courseid' => $c1->id]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u1->id), [$u1ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u2->id), [$u2ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u3->id), [$c1ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u4->id), []);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u5->id), []);
|
||||
|
||||
$ccg->create_user_evidence(['userid' => $u4->id]);
|
||||
$ue = $ccg->create_user_evidence(['userid' => $u4->id]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u1->id), [$u1ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u2->id), [$u2ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u3->id), [$c1ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u4->id), [$u4ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u5->id), []);
|
||||
|
||||
// A user editing a context relationship.
|
||||
$this->setUser($u5);
|
||||
$ccg->create_user_evidence_competency(['userevidenceid' => $ue->get('id'), 'competencyid' => $comp1->get('id')]);
|
||||
$this->setAdminUser();
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u1->id), [$u1ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u2->id), [$u2ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u3->id), [$c1ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u4->id), [$u4ctx]);
|
||||
$this->assert_contextlist(provider::get_contexts_for_userid($u5->id), [$u4ctx]);
|
||||
}
|
||||
|
||||
public function test_get_users_in_context_with_actual_data_and_actual_data_is_goooood() {
|
||||
@@ -2598,7 +2615,7 @@ class provider_test extends provider_testcase {
|
||||
$expectedids = array_unique(array_map(function($item) {
|
||||
return $item instanceof \context ? $item->id : $id;
|
||||
}, $expectedcontextsorids));
|
||||
$this->assert_array_match($contextids, $expectedids);
|
||||
$this->assert_array_match($expectedids, $contextids);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,15 +14,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Bulk edit activity completion form
|
||||
*
|
||||
* @package core_completion
|
||||
* @copyright 2017 Marina Glancy
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
use core_completion\manager;
|
||||
|
||||
/**
|
||||
* Bulk edit activity completion form
|
||||
@@ -80,26 +72,11 @@ class core_completion_bulkedit_form extends core_completion_edit_base_form {
|
||||
}
|
||||
|
||||
$cm = reset($this->cms);
|
||||
$course = $this->course;
|
||||
$modname = $cm->modname;
|
||||
|
||||
$modmoodleform = "$CFG->dirroot/mod/$modname/mod_form.php";
|
||||
if (file_exists($modmoodleform)) {
|
||||
require_once($modmoodleform);
|
||||
} else {
|
||||
throw new \moodle_exception('noformdesc');
|
||||
}
|
||||
|
||||
list($cmrec, $context, $module, $data, $cw) = get_moduleinfo_data($cm, $course);
|
||||
$data->return = 0;
|
||||
$data->sr = 0;
|
||||
$data->update = $modname;
|
||||
|
||||
// Initialise the form but discard all JS requirements it adds, our form has already added them.
|
||||
$mformclassname = 'mod_'.$modname.'_mod_form';
|
||||
$PAGE->start_collecting_javascript_requirements();
|
||||
$this->_moduleform = new $mformclassname($data, 0, $cmrec, $course);
|
||||
$PAGE->end_collecting_javascript_requirements();
|
||||
$this->_moduleform = manager::get_module_form(
|
||||
modname: $cm->modname,
|
||||
course: $this->course,
|
||||
cm: $cm,
|
||||
);
|
||||
|
||||
return $this->_moduleform;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use core_completion\manager;
|
||||
|
||||
/**
|
||||
* Default activity completion form
|
||||
*
|
||||
@@ -68,34 +70,16 @@ class core_completion_defaultedit_form extends core_completion_edit_base_form {
|
||||
* @return moodleform_mod|null
|
||||
*/
|
||||
protected function get_module_form() {
|
||||
global $CFG, $PAGE;
|
||||
|
||||
if ($this->_moduleform) {
|
||||
return $this->_moduleform;
|
||||
}
|
||||
|
||||
$modnames = array_keys($this->get_module_names());
|
||||
$modname = $modnames[0];
|
||||
$course = $this->course;
|
||||
|
||||
$modmoodleform = "$CFG->dirroot/mod/$modname/mod_form.php";
|
||||
if (file_exists($modmoodleform)) {
|
||||
require_once($modmoodleform);
|
||||
} else {
|
||||
throw new \moodle_exception('noformdesc');
|
||||
}
|
||||
|
||||
list($module, $context, $cw, $cmrec, $data) = prepare_new_moduleinfo_data($course, $modname, 0, $this->get_suffix());
|
||||
$data->return = 0;
|
||||
$data->sr = 0;
|
||||
$data->add = $modname;
|
||||
|
||||
// Initialise the form but discard all JS requirements it adds, our form has already added them.
|
||||
$mformclassname = 'mod_'.$modname.'_mod_form';
|
||||
$PAGE->start_collecting_javascript_requirements();
|
||||
$this->_moduleform = new $mformclassname($data, 0, $cmrec, $course);
|
||||
$this->_moduleform->set_suffix('_' . $modname);
|
||||
$PAGE->end_collecting_javascript_requirements();
|
||||
$this->_moduleform = manager::get_module_form(
|
||||
modname: $modnames[0],
|
||||
course: $this->course,
|
||||
suffix: $this->get_suffix(),
|
||||
);
|
||||
|
||||
return $this->_moduleform;
|
||||
}
|
||||
|
||||
@@ -109,10 +109,15 @@ abstract class core_completion_edit_base_form extends moodleform {
|
||||
$component = "mod_{$modnames[0]}";
|
||||
$itemnames = \core_grades\component_gradeitems::get_itemname_mapping_for_component($component);
|
||||
$hascustomrules = count($itemnames) > 1;
|
||||
$customcompletionelements = [];
|
||||
|
||||
try {
|
||||
// Add completion rules from the module form to this form.
|
||||
$moduleform = $this->get_module_form();
|
||||
// If the module doesn't return a form for any reason, we don't continue checking.
|
||||
if (!$moduleform) {
|
||||
return [false, []];
|
||||
}
|
||||
$moduleform->_form = $this->_form;
|
||||
if ($customcompletionelements = $moduleform->{$function}()) {
|
||||
$hascustomrules = true;
|
||||
@@ -135,7 +140,7 @@ abstract class core_completion_edit_base_form extends moodleform {
|
||||
} catch (Exception $e) {
|
||||
debugging('Could not add custom completion rule of module ' . $modnames[0] .
|
||||
' to this form, this has to be fixed by the developer', DEBUG_DEVELOPER);
|
||||
return [$hascustomrules, $customcompletionelements];
|
||||
return [false, $customcompletionelements];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -238,7 +238,12 @@ class manager {
|
||||
$context = $this->get_context();
|
||||
$canmanage = has_capability('moodle/course:manageactivities', $context);
|
||||
$course = get_course($this->courseid);
|
||||
$availablemodules = [];
|
||||
foreach ($data->modules as $module) {
|
||||
$libfile = "$CFG->dirroot/mod/$module->name/lib.php";
|
||||
if (!file_exists($libfile)) {
|
||||
continue;
|
||||
}
|
||||
$module->icon = $OUTPUT->image_url('monologo', $module->name)->out();
|
||||
$module->formattedname = format_string(get_string('modulename', 'mod_' . $module->name),
|
||||
true, ['context' => $context]);
|
||||
@@ -248,13 +253,13 @@ class manager {
|
||||
$defaults->modname = $module->name;
|
||||
$module->completionstatus = $this->get_completion_detail($defaults);
|
||||
}
|
||||
$availablemodules[] = $module;
|
||||
}
|
||||
// Order modules by displayed name.
|
||||
$modules = (array) $data->modules;
|
||||
usort($modules, function($a, $b) {
|
||||
usort($availablemodules, function($a, $b) {
|
||||
return strcmp($a->formattedname, $b->formattedname);
|
||||
});
|
||||
$data->modules = $modules;
|
||||
$data->modules = $availablemodules;
|
||||
|
||||
return $data;
|
||||
}
|
||||
@@ -627,4 +632,57 @@ class manager {
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a mod_form of the given module.
|
||||
*
|
||||
* @param string $modname Module to get the form from.
|
||||
* @param stdClass $course Course object.
|
||||
* @param ?cm_info $cm cm_info object to use.
|
||||
* @param string $suffix The suffix to add to the name of the completion rules.
|
||||
* @return ?\moodleform_mod The moodleform_mod object if everything goes fine. Null otherwise.
|
||||
*/
|
||||
public static function get_module_form(
|
||||
string $modname,
|
||||
stdClass $course,
|
||||
?cm_info $cm = null,
|
||||
string $suffix = ''
|
||||
): ?\moodleform_mod {
|
||||
global $CFG, $PAGE;
|
||||
|
||||
$modmoodleform = "$CFG->dirroot/mod/$modname/mod_form.php";
|
||||
if (file_exists($modmoodleform)) {
|
||||
require_once($modmoodleform);
|
||||
} else {
|
||||
throw new \moodle_exception('noformdesc');
|
||||
}
|
||||
|
||||
if ($cm) {
|
||||
[$cmrec, $context, $module, $data, $cw] = get_moduleinfo_data($cm, $course);
|
||||
$data->update = $modname;
|
||||
} else {
|
||||
[$module, $context, $cw, $cmrec, $data] = prepare_new_moduleinfo_data($course, $modname, 0, $suffix);
|
||||
$data->add = $modname;
|
||||
}
|
||||
$data->return = 0;
|
||||
$data->sr = 0;
|
||||
|
||||
// Initialise the form but discard all JS requirements it adds, our form has already added them.
|
||||
$mformclassname = 'mod_'.$modname.'_mod_form';
|
||||
$PAGE->start_collecting_javascript_requirements();
|
||||
try {
|
||||
$moduleform = new $mformclassname($data, 0, $cmrec, $course);
|
||||
if (!$cm) {
|
||||
$moduleform->set_suffix('_' . $modname);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// The form class has thrown an error when instantiating.
|
||||
// This could happen because some conditions for the module are not met.
|
||||
$moduleform = null;
|
||||
} finally {
|
||||
$PAGE->end_collecting_javascript_requirements();
|
||||
}
|
||||
|
||||
return $moduleform;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,8 +179,10 @@ class completion_completion extends data_object {
|
||||
// Notify user.
|
||||
$course = get_course($data->course);
|
||||
$messagesubject = get_string('coursecompleted', 'completion');
|
||||
$options = new stdClass();
|
||||
$options->context = context_course::instance($course->id);
|
||||
$a = [
|
||||
'coursename' => get_course_display_name_for_list($course),
|
||||
'coursename' => format_string(get_course_display_name_for_list($course), true, $options),
|
||||
'courselink' => (string) new moodle_url('/course/view.php', array('id' => $course->id)),
|
||||
];
|
||||
$messagebody = get_string('coursecompletedmessage', 'completion', $a);
|
||||
|
||||
@@ -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 click on "Expand all" "link" in the "region-main" "region"
|
||||
And I set the field "Add requirements" to "1"
|
||||
And I set the field "completionusegrade" to "1"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user