Compare commits
509 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf687f4357 | |||
| e32ae38439 | |||
| 4e09dc64b7 | |||
| fcefa38092 | |||
| 61f218ae47 | |||
| 6bafc320c0 | |||
| a22a46a689 | |||
| 9e6f90f1cf | |||
| 62ed52fdc4 | |||
| ad45df4dc2 | |||
| c65e8a5900 | |||
| 796a7ea458 | |||
| 3c5d8d6602 | |||
| 2ad366c869 | |||
| 55ba3eece2 | |||
| fb990d26d4 | |||
| 96dbdc2ad9 | |||
| d976ae38f5 | |||
| 675c0451f9 | |||
| f5e8e0b08a | |||
| c9da87aa50 | |||
| c070acd2eb | |||
| 647aa0625f | |||
| a4e65c3276 | |||
| 225e176962 | |||
| 75b5301c9a | |||
| 6d780ff6d5 | |||
| 5ed7751049 | |||
| 8d21ca65d4 | |||
| 20644f8c6a | |||
| 22c36cd1b6 | |||
| 014fd48298 | |||
| 6006347f62 | |||
| fc1d54365e | |||
| 41536c1586 | |||
| 5b52f17db3 | |||
| a44b5cbd84 | |||
| 3eca8fedf0 | |||
| 6136ba0221 | |||
| 9a6874fee7 | |||
| d5483a9d21 | |||
| e4079005ef | |||
| 9c077351e5 | |||
| abe082493e | |||
| 5ea56fd371 | |||
| 2cd1ef8c97 | |||
| 35cf368176 | |||
| ed3a03b439 | |||
| 0b7412e092 | |||
| 3f19c3cfb4 | |||
| 2c2232ce23 | |||
| 1bd4817843 | |||
| 5673b3b48a | |||
| 686468bae6 | |||
| 30d3dad3bb | |||
| 4fa12efce0 | |||
| 38f595a0df | |||
| cdaae17f19 | |||
| 6a0f3a8736 | |||
| 201946c82c | |||
| 9950de034a | |||
| e43a539e10 | |||
| 80474ee9e8 | |||
| cce1fe41a6 | |||
| ae0d7f7089 | |||
| 77d5634729 | |||
| f2d2912241 | |||
| 28de453200 | |||
| d571624920 | |||
| 12659cd7f1 | |||
| 84e737e3dd | |||
| 64f54e0dc4 | |||
| 06188c7eb6 | |||
| c40fde2eae | |||
| 22ec8c83b5 | |||
| b2507972df | |||
| f524ec2aaa | |||
| 050cede6d8 | |||
| ac1fc5575a | |||
| 3870922e61 | |||
| 9673018068 | |||
| f98f08c00a | |||
| 22120a505a | |||
| 5466990ffc | |||
| ae77ba7af5 | |||
| 3291f83c81 | |||
| 5963365517 | |||
| 3f7b3abaa9 | |||
| a1d6d0ceb2 | |||
| 78f3ac2a67 | |||
| 9ff01f7cb5 | |||
| a4c62ac8c1 | |||
| fa07a148ef | |||
| 4c94e4947a | |||
| 8484ab9ec9 | |||
| 56e572a73e | |||
| 94de68f9b8 | |||
| 89d61861a6 | |||
| f8f1a2dd86 | |||
| 8039c06800 | |||
| b59c6dbd51 | |||
| 7ab1221629 | |||
| 9818289b15 | |||
| 274e050ab4 | |||
| f9aa942b2c | |||
| d4b9e6613f | |||
| 7098d7cb9f | |||
| 945469f0e1 | |||
| 0470c9cc4e | |||
| 1ea8b348dd | |||
| a65a5570a3 | |||
| da15ac0233 | |||
| 07091e866d | |||
| 37d34c2aa4 | |||
| 4934b00dca | |||
| 0f4e00f527 | |||
| d8ba1a5db5 | |||
| 1a5c2aea69 | |||
| e2df259a6a | |||
| c7c4882929 | |||
| bd3600a28e | |||
| 88955a1984 | |||
| 795069eb94 | |||
| e061d00010 | |||
| b6e8c88f0b | |||
| c96e66a40a | |||
| 23e71ce7a5 | |||
| a7c8680ec3 | |||
| 2c6df5b7f0 | |||
| 316575d3b3 | |||
| 9a856f83fe | |||
| c7c98440cf | |||
| 1674e5f87c | |||
| 98af061024 | |||
| b71d42cfb4 | |||
| 337d0eb9e8 | |||
| 316d6c9de5 | |||
| 3e32822706 | |||
| 8314f2e9a2 | |||
| ffe5f336d3 | |||
| 1c45c9200a | |||
| 6c8328eb4e | |||
| 08a62243c5 | |||
| 4feb95d263 | |||
| e3e5bdfbe4 | |||
| c074850dc4 | |||
| 5bfbbb34be | |||
| da231a51e0 | |||
| 47fe981b8e | |||
| a4087a6cab | |||
| 965a8fdfc4 | |||
| ac3c2ec5ba | |||
| 68a72085f2 | |||
| 1131d828f2 | |||
| a7b6456475 | |||
| b9a527603d | |||
| 1e66dd6de0 | |||
| 74b8e6a751 | |||
| c137a7d306 | |||
| 7051341ba6 | |||
| 1db8e1bab4 | |||
| 1da6538756 | |||
| 5d580ac95c | |||
| f76355db90 | |||
| 1cdf348ac5 | |||
| 91e4d8f3d8 | |||
| f8c7167b5d | |||
| 94ec80ae18 | |||
| 6e1a330c81 | |||
| 752bd12fd4 | |||
| 6d7e1cfecb | |||
| daa86448e9 | |||
| 6536a3ff9b | |||
| 604b2a3b58 | |||
| 4e3837eda1 | |||
| 3b7037656e | |||
| fb6cdb3a9f | |||
| 3e718aff10 | |||
| 95fce73f16 | |||
| 07dbce000c | |||
| 9b82b174d0 | |||
| 5d117508d8 | |||
| 02d491fe99 | |||
| 72c744dd22 | |||
| 58fe4d2cbd | |||
| 49361f9a24 | |||
| 129c9d4093 | |||
| 8475b74d0c | |||
| f246d30831 | |||
| 50f32f791b | |||
| 88779cc689 | |||
| f8f3286267 | |||
| b167974308 | |||
| 20ecbc885f | |||
| 4980533b75 | |||
| b6d5a6c619 | |||
| 50e3ae359b | |||
| cf65056e46 | |||
| 6c47187329 | |||
| a471a106c4 | |||
| 5ce5abcd67 | |||
| 73aac2ea23 | |||
| e8ae8a8a76 | |||
| 39901e1577 | |||
| 7a87e8a911 | |||
| 5fd324e17e | |||
| 50889d5519 | |||
| 1cc8177b73 | |||
| 8b33494324 | |||
| 8e2494b4b2 | |||
| 941a4c10cd | |||
| 853fc48867 | |||
| 4ca4651f20 | |||
| 36223fbee8 | |||
| 9d08ae36e9 | |||
| ed34ac0a7c | |||
| ea02bf99d7 | |||
| 52491c6cc7 | |||
| 5c39f8969d | |||
| c86fa207a2 | |||
| 631b1f77f0 | |||
| ec67d8b676 | |||
| 9ceb49dca0 | |||
| 147087cdd2 | |||
| 9800e5ca6f | |||
| ab0f20b63b | |||
| 739a18df53 | |||
| bb66bd562f | |||
| 3aba5ed0ce | |||
| 3ec126df58 | |||
| 59964c5c95 | |||
| 61e4180191 | |||
| 23739a476a | |||
| 8f39f26b9e | |||
| 012dde7fe7 | |||
| 1d77fbd497 | |||
| d3ce732601 | |||
| 838193097d | |||
| 0c90bc8cd3 | |||
| 80011bdf57 | |||
| 17c23edcd1 | |||
| 6f0b92e97c | |||
| e0f6182508 | |||
| d1084b4c26 | |||
| 805361d871 | |||
| 2e3d05e591 | |||
| 8d68b0b251 | |||
| 5b8b45410c | |||
| efc431ba0d | |||
| 9df4aa7210 | |||
| 8481609ba4 | |||
| 194406e923 | |||
| c4665bcf95 | |||
| c70d7d0386 | |||
| 4a3a673d1e | |||
| 31260f8667 | |||
| cb3deb89c1 | |||
| b21aad0837 | |||
| 43b11cfc9e | |||
| 8ba10c6666 | |||
| b7b45f8c0b | |||
| a2ff87ba44 | |||
| 53a3991927 | |||
| 6b502fbc2b | |||
| 8509f1feb4 | |||
| b692832347 | |||
| a5a6a9c91c | |||
| 25c09b1b1d | |||
| f00ddea3fc | |||
| 4caf331102 | |||
| 0705bb0c07 | |||
| 4748f89945 | |||
| f0abaec208 | |||
| 2933c4e061 | |||
| b808efac50 | |||
| 0df30e445f | |||
| d4f03eb0b1 | |||
| 635b4db751 | |||
| 1821997b7c | |||
| a97fde462d | |||
| 9bcfb74256 | |||
| c0fdc91ea3 | |||
| da1d1af843 | |||
| 85d5216840 | |||
| 2337123e89 | |||
| 95eb7d8343 | |||
| 720a26266a | |||
| 1187f828b7 | |||
| 37906d63f9 | |||
| 870e202546 | |||
| 50a72fbcd6 | |||
| b3172cc0fd | |||
| dc39cee4f9 | |||
| f05ff7a66d | |||
| cb2a49009c | |||
| b541ee20fb | |||
| c975694cf5 | |||
| 8c39267d92 | |||
| 45f1ddae59 | |||
| a45fa24362 | |||
| e127e819e6 | |||
| ce5337c902 | |||
| b74030550e | |||
| 2cfcae67a0 | |||
| dbb784d1ab | |||
| be941e5a05 | |||
| 87080161e0 | |||
| 62931543fb | |||
| 3cb6a5a3b2 | |||
| 765aef276c | |||
| 2da65dae97 | |||
| 4be6148785 | |||
| 5e4ffd0559 | |||
| 99d5aff09b | |||
| ae31e9dfa8 | |||
| d077ffd3a5 | |||
| c61f9a4484 | |||
| 37ef74572d | |||
| d08e7b7e8a | |||
| 4441cfe9ce | |||
| 9dd7ac3bc3 | |||
| 2a912052bc | |||
| addc07be5a | |||
| 6aaa78f044 | |||
| 6ffe3ecc9f | |||
| c4b472700b | |||
| 414a461e41 | |||
| 952d572921 | |||
| fc01f863d3 | |||
| 1b43f6b06d | |||
| 86f297c7c2 | |||
| 21bac71bef | |||
| b400040cff | |||
| 0846b4af08 | |||
| d927703799 | |||
| 8fedcb49a5 | |||
| 0c9fe38643 | |||
| 5c7966eacf | |||
| 0ee03dd15a | |||
| 6dbf1dee71 | |||
| a884eea913 | |||
| b41b74aacb | |||
| fa0e63f9da | |||
| 306eaa606f | |||
| 9ff79ab562 | |||
| 4011bfc71f | |||
| dedb1592cb | |||
| 603e31b820 | |||
| 3dab17b872 | |||
| e81b6194e6 | |||
| b54c18392f | |||
| 7960af6567 | |||
| f24cc998f0 | |||
| 07265da01c | |||
| 6b2bbcbb0e | |||
| c1bc5df576 | |||
| 750218868a | |||
| 18aabbd5d5 | |||
| 4f5e2fc266 | |||
| ba801e68d7 | |||
| fbd65fabb2 | |||
| ee0f61cde4 | |||
| 5779862ee7 | |||
| 9a49f51b15 | |||
| 59f947667a | |||
| 6733f41ed1 | |||
| 867d9497f3 | |||
| a9895627a4 | |||
| 1a75be0645 | |||
| 53c3b1bacc | |||
| 730714bc78 | |||
| 4a0abc17d4 | |||
| 7eaadb4bdb | |||
| 2f88edd0c9 | |||
| 03721f06fb | |||
| c0b25af986 | |||
| d869324747 | |||
| 1b1cf1b950 | |||
| d66d85a4d7 | |||
| 67d4f4ea65 | |||
| fe1b4bee20 | |||
| 6292c6299b | |||
| 22177c5c3e | |||
| 9f2e264fd8 | |||
| e1b1a447b5 | |||
| eaf2d6a809 | |||
| a8e2b5cea1 | |||
| 7fda2e9bac | |||
| ea01c38f2c | |||
| ee0a8161dd | |||
| 8adff28414 | |||
| 020810aea0 | |||
| 56930d1d67 | |||
| 2ec76277dc | |||
| bfff57b297 | |||
| 5e7c5b9cec | |||
| caa236c415 | |||
| 52a77683aa | |||
| 6afb3736f8 | |||
| 96582b63db | |||
| a5e149a1b7 | |||
| 7cf3a406ff | |||
| 25d1533b27 | |||
| 462c6e9e2c | |||
| 1db5a1205e | |||
| c3c7b02867 | |||
| 7d1af3ec1d | |||
| 5efefd458e | |||
| ebc198cfb8 | |||
| 1c348b2e8d | |||
| f18444a5d1 | |||
| 6fd48eff30 | |||
| 68082ac364 | |||
| 9edebd4951 | |||
| adfe298af8 | |||
| cd6c7e0068 | |||
| d11806d957 | |||
| 6a5c1ab6f8 | |||
| e453665f7d | |||
| 3f73b4f487 | |||
| 4dbc8e7655 | |||
| 1fb38c5cf6 | |||
| 71b452912a | |||
| 1c9edf3920 | |||
| c4f7a4bc62 | |||
| 9d72d1a9e6 | |||
| 198b653bf7 | |||
| 77a359ca7b | |||
| bf1e44117b | |||
| f8df624e37 | |||
| 704c30b1fb | |||
| a178e09bfb | |||
| 48096011e3 | |||
| 7ef572d39e | |||
| 75ab679716 | |||
| 2dc1afe547 | |||
| 115f4461ce | |||
| c0cf6abf34 | |||
| ff6ea1e369 | |||
| c5c42e96d2 | |||
| 39fd21e4ff | |||
| 49f70858e9 | |||
| 64d75c9a1a | |||
| 695499c681 | |||
| d8c3b7e662 | |||
| 6adc5e89b0 | |||
| c9eeb1a7c1 | |||
| 68e4a7dac8 | |||
| 73bbb3e7a8 | |||
| d00045e8f5 | |||
| 7a8b3702e3 | |||
| 887f9c3fc6 | |||
| 28b0d9256f | |||
| 2e6729e923 | |||
| d3a2788437 | |||
| c04559ccb6 | |||
| a6e4fbe122 | |||
| c1ed11df7a | |||
| 1edd36e905 | |||
| 91aee9939b | |||
| a585ee9432 | |||
| bb0acb3e9c | |||
| 02f08b16c5 | |||
| e206c3b8f2 | |||
| 3246d41cb2 | |||
| 8594f5e8ae | |||
| c0842ca500 | |||
| c670210a1d | |||
| fd908d09fd | |||
| d859e8774c | |||
| a86318bd92 | |||
| 77b19b828b | |||
| d046109d4d | |||
| bc8d03c68d | |||
| f394d9efae | |||
| b67c73c86b | |||
| 9941fbdae4 | |||
| aa23048889 | |||
| c4b8e4ca14 | |||
| e5ec4988b6 | |||
| 35ce17025a | |||
| fc1ddb3289 | |||
| 85b9cf9ce4 | |||
| 05bb501b4f | |||
| 116d41cdfd | |||
| a5cff8a55b | |||
| 06e0263936 | |||
| de98c397f3 | |||
| f091f67f81 | |||
| 7cfbdd0422 | |||
| 089100fdb3 | |||
| 68e85534a8 | |||
| 0f41a0280d | |||
| fd9466fceb | |||
| 6cb1f99459 | |||
| 8bc3576bb3 | |||
| 4158b39809 | |||
| c7afe02401 | |||
| 37524fa1e6 | |||
| 44c7386d16 | |||
| c3f4abed6f | |||
| 2acb24d84e | |||
| 6783597dc0 | |||
| 8c3f81aa69 | |||
| 3dd509b06b | |||
| da1a4189e1 | |||
| 213abaaf97 | |||
| 9e9c0912e3 | |||
| ad7d0f05de |
@@ -65,6 +65,7 @@ lib/validateurlsyntax.php
|
||||
lib/amd/src/popper.js
|
||||
lib/geopattern-php/
|
||||
lib/php-jwt/
|
||||
lib/mdn-polyfills/
|
||||
media/player/videojs/amd/src/video-lazy.js
|
||||
media/player/videojs/amd/src/Youtube-lazy.js
|
||||
media/player/videojs/videojs/
|
||||
|
||||
@@ -66,6 +66,7 @@ lib/validateurlsyntax.php
|
||||
lib/amd/src/popper.js
|
||||
lib/geopattern-php/
|
||||
lib/php-jwt/
|
||||
lib/mdn-polyfills/
|
||||
media/player/videojs/amd/src/video-lazy.js
|
||||
media/player/videojs/amd/src/Youtube-lazy.js
|
||||
media/player/videojs/videojs/
|
||||
|
||||
+3
-3
@@ -2,8 +2,6 @@
|
||||
# process (which uses our internal CI system) this file is here for the benefit
|
||||
# of community developers git clones - see MDL-51458.
|
||||
|
||||
sudo: required
|
||||
|
||||
# We currently disable Travis notifications entirely until https://github.com/travis-ci/travis-ci/issues/4976
|
||||
# is fixed.
|
||||
notifications:
|
||||
@@ -11,6 +9,8 @@ notifications:
|
||||
|
||||
language: php
|
||||
|
||||
os: linux
|
||||
|
||||
dist: xenial
|
||||
|
||||
services:
|
||||
@@ -41,7 +41,7 @@ env:
|
||||
# Perform an upgrade test too.
|
||||
- DB=pgsql TASK=UPGRADE
|
||||
|
||||
matrix:
|
||||
jobs:
|
||||
# Enable fast finish.
|
||||
# This will fail the build if a single job fails (except those in allow_failures).
|
||||
# It will not stop the jobs from running.
|
||||
|
||||
+30
-13
@@ -112,7 +112,13 @@ module.exports = function(grunt) {
|
||||
eslint: {
|
||||
// Even though warnings dont stop the build we don't display warnings by default because
|
||||
// at this moment we've got too many core warnings.
|
||||
options: {quiet: !grunt.option('show-lint-warnings')},
|
||||
// To display warnings call: grunt eslint --show-lint-warnings
|
||||
// To fail on warnings call: grunt eslint --max-lint-warnings=0
|
||||
// Also --max-lint-warnings=-1 can be used to display warnings but not fail.
|
||||
options: {
|
||||
quiet: (!grunt.option('show-lint-warnings')) && (typeof grunt.option('max-lint-warnings') === 'undefined'),
|
||||
maxWarnings: ((typeof grunt.option('max-lint-warnings') !== 'undefined') ? grunt.option('max-lint-warnings') : -1)
|
||||
},
|
||||
amd: {src: amdSrc},
|
||||
// Check YUI module source files.
|
||||
yui: {src: ['**/yui/src/**/*.js', '!*/**/yui/src/*/meta/*.js']}
|
||||
@@ -151,6 +157,9 @@ module.exports = function(grunt) {
|
||||
tasks: ['yui']
|
||||
},
|
||||
gherkinlint: {
|
||||
options: {
|
||||
nospawn: false,
|
||||
},
|
||||
files: ['**/tests/behat/*.feature'],
|
||||
tasks: ['gherkinlint']
|
||||
}
|
||||
@@ -293,19 +302,27 @@ module.exports = function(grunt) {
|
||||
};
|
||||
|
||||
tasks.gherkinlint = function() {
|
||||
var done = this.async(),
|
||||
options = grunt.config('gherkinlint.options');
|
||||
const done = this.async();
|
||||
const options = grunt.config('gherkinlint.options');
|
||||
|
||||
var args = grunt.file.expand(options.files);
|
||||
args.unshift(path.normalize(__dirname + '/node_modules/.bin/gherkin-lint'));
|
||||
grunt.util.spawn({
|
||||
cmd: 'node',
|
||||
args: args,
|
||||
opts: {stdio: 'inherit', env: process.env}
|
||||
}, function(error, result, code) {
|
||||
// Propagate the exit code.
|
||||
done(code === 0);
|
||||
});
|
||||
// Grab the gherkin-lint linter and required scaffolding.
|
||||
const linter = require('gherkin-lint/src/linter.js');
|
||||
const featureFinder = require('gherkin-lint/src/feature-finder.js');
|
||||
const configParser = require('gherkin-lint/src/config-parser.js');
|
||||
const formatter = require('gherkin-lint/src/formatters/stylish.js');
|
||||
|
||||
// Run the linter.
|
||||
const results = linter.lint(
|
||||
featureFinder.getFeatureFiles(grunt.file.expand(options.files)),
|
||||
configParser.getConfiguration(configParser.defaultConfigFileName)
|
||||
);
|
||||
|
||||
// Print the results out uncondtionally.
|
||||
formatter.printResults(results);
|
||||
|
||||
// Report on the results.
|
||||
// The done function takes a bool whereby a falsey statement causes the task to fail.
|
||||
done(results.every(result => result.errors.length === 0));
|
||||
};
|
||||
|
||||
tasks.startup = function() {
|
||||
|
||||
+25
-8
@@ -226,7 +226,7 @@ if (!core_tables_exist()) {
|
||||
}
|
||||
if (empty($confirmrelease)) {
|
||||
require_once($CFG->libdir.'/environmentlib.php');
|
||||
list($envstatus, $environment_results) = check_moodle_environment(normalize_version($release), ENV_SELECT_RELEASE);
|
||||
list($envstatus, $environmentresults) = check_moodle_environment(normalize_version($release), ENV_SELECT_RELEASE);
|
||||
$strcurrentrelease = get_string('currentrelease');
|
||||
|
||||
$PAGE->navbar->add($strcurrentrelease);
|
||||
@@ -235,7 +235,7 @@ if (!core_tables_exist()) {
|
||||
$PAGE->set_cacheable(false);
|
||||
|
||||
$output = $PAGE->get_renderer('core', 'admin');
|
||||
echo $output->install_environment_page($maturity, $envstatus, $environment_results, $release);
|
||||
echo $output->install_environment_page($maturity, $envstatus, $environmentresults, $release);
|
||||
die();
|
||||
}
|
||||
|
||||
@@ -358,9 +358,9 @@ if (!$cache and $version > $CFG->version) { // upgrade
|
||||
echo $output->upgrade_confirm_page($a->newversion, $maturity, $testsite);
|
||||
die();
|
||||
|
||||
} else if (empty($confirmrelease)){
|
||||
} else if (empty($confirmrelease)) {
|
||||
require_once($CFG->libdir.'/environmentlib.php');
|
||||
list($envstatus, $environment_results) = check_moodle_environment($release, ENV_SELECT_RELEASE);
|
||||
list($envstatus, $environmentresults) = check_moodle_environment($release, ENV_SELECT_RELEASE);
|
||||
$strcurrentrelease = get_string('currentrelease');
|
||||
|
||||
$PAGE->navbar->add($strcurrentrelease);
|
||||
@@ -368,7 +368,7 @@ if (!$cache and $version > $CFG->version) { // upgrade
|
||||
$PAGE->set_heading($strcurrentrelease);
|
||||
$PAGE->set_cacheable(false);
|
||||
|
||||
echo $output->upgrade_environment_page($release, $envstatus, $environment_results);
|
||||
echo $output->upgrade_environment_page($release, $envstatus, $environmentresults);
|
||||
die();
|
||||
|
||||
} else if (empty($confirmplugins)) {
|
||||
@@ -533,7 +533,10 @@ if (!$cache and $branch <> $CFG->branch) { // Update the branch
|
||||
|
||||
if (!$cache and moodle_needs_upgrading()) {
|
||||
|
||||
$PAGE->set_url(new moodle_url($PAGE->url, array('confirmplugincheck' => $confirmplugins)));
|
||||
$PAGE->set_url(new moodle_url($PAGE->url, array(
|
||||
'confirmrelease' => $confirmrelease,
|
||||
'confirmplugincheck' => $confirmplugins,
|
||||
)));
|
||||
|
||||
check_upgrade_key($upgradekeyhash);
|
||||
|
||||
@@ -543,7 +546,21 @@ if (!$cache and moodle_needs_upgrading()) {
|
||||
$pluginman = core_plugin_manager::instance();
|
||||
$output = $PAGE->get_renderer('core', 'admin');
|
||||
|
||||
if (!$confirmplugins) {
|
||||
if (empty($confirmrelease)) {
|
||||
require_once($CFG->libdir . '/environmentlib.php');
|
||||
|
||||
list($envstatus, $environmentresults) = check_moodle_environment($release, ENV_SELECT_RELEASE);
|
||||
$strcurrentrelease = get_string('currentrelease');
|
||||
|
||||
$PAGE->navbar->add($strcurrentrelease);
|
||||
$PAGE->set_title($strcurrentrelease);
|
||||
$PAGE->set_heading($strcurrentrelease);
|
||||
$PAGE->set_cacheable(false);
|
||||
|
||||
echo $output->upgrade_environment_page($release, $envstatus, $environmentresults);
|
||||
die();
|
||||
|
||||
} else if (!$confirmplugins) {
|
||||
$strplugincheck = get_string('plugincheck');
|
||||
|
||||
$PAGE->navbar->add($strplugincheck);
|
||||
@@ -802,7 +819,7 @@ $SESSION->admin_critical_warning = ($insecuredataroot==INSECURE_DATAROOT_ERROR);
|
||||
$adminroot = admin_get_root();
|
||||
|
||||
// Check if there are any new admin settings which have still yet to be set
|
||||
if (any_new_admin_settings($adminroot)){
|
||||
if (any_new_admin_settings($adminroot)) {
|
||||
redirect('upgradesettings.php');
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -83,7 +83,7 @@ if (($form = data_submitted()) && confirm_sesskey()) {
|
||||
}
|
||||
} else {
|
||||
$newsettings = array();
|
||||
if (array_key_exists($componentprovidersetting, $form)) {
|
||||
if (property_exists($form, $componentprovidersetting)) {
|
||||
// We must be processing loggedin or loggedoff checkboxes.
|
||||
// Store defained comma-separated processors as setting value.
|
||||
// Using array_filter eliminates elements set to 0 above.
|
||||
|
||||
@@ -45,7 +45,7 @@ $error = optional_param('error', '', PARAM_ALPHANUM);
|
||||
|
||||
admin_externalpage_setup('registrationmoodleorg');
|
||||
|
||||
if ($url !== HUB_MOODLEORGHUBURL) {
|
||||
if (parse_url($url, PHP_URL_HOST) !== parse_url(HUB_MOODLEORGHUBURL, PHP_URL_HOST)) {
|
||||
// Allow other plugins to confirm registration on custom hubs. Plugins implementing this
|
||||
// callback need to redirect or exit. See https://docs.moodle.org/en/Hub_registration .
|
||||
$callbacks = get_plugins_with_function('hub_registration');
|
||||
|
||||
@@ -39,7 +39,7 @@ $token = optional_param('token', '', PARAM_TEXT);
|
||||
|
||||
admin_externalpage_setup('registrationmoodleorg');
|
||||
|
||||
if ($url !== HUB_MOODLEORGHUBURL) {
|
||||
if (parse_url($url, PHP_URL_HOST) !== parse_url(HUB_MOODLEORGHUBURL, PHP_URL_HOST)) {
|
||||
// Allow other plugins to renew registration on custom hubs. Plugins implementing this
|
||||
// callback need to redirect or exit. See https://docs.moodle.org/en/Hub_registration .
|
||||
$callbacks = get_plugins_with_function('hub_registration');
|
||||
|
||||
+49
-40
@@ -361,14 +361,15 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
$output = '';
|
||||
|
||||
if ($checker->enabled()) {
|
||||
$output .= $this->container_start('checkforupdates');
|
||||
$output .= $this->container_start('checkforupdates mb-4');
|
||||
$output .= $this->single_button(
|
||||
new moodle_url($reloadurl, array('fetchupdates' => 1)),
|
||||
get_string('checkforupdates', 'core_plugin')
|
||||
);
|
||||
if ($timefetched = $checker->get_last_timefetched()) {
|
||||
$timefetched = userdate($timefetched, get_string('strftimedatetime', 'core_langconfig'));
|
||||
$output .= $this->container(get_string('checkforupdateslast', 'core_plugin', $timefetched), 'lasttimefetched');
|
||||
$output .= $this->container(get_string('checkforupdateslast', 'core_plugin', $timefetched),
|
||||
'lasttimefetched small text-muted mt-1');
|
||||
}
|
||||
$output .= $this->container_end();
|
||||
}
|
||||
@@ -823,7 +824,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
}
|
||||
}
|
||||
|
||||
$updateinfo .= $this->container_start('checkforupdates');
|
||||
$updateinfo .= $this->container_start('checkforupdates mt-1');
|
||||
$fetchurl = new moodle_url('/admin/index.php', array('fetchupdates' => 1, 'sesskey' => sesskey(), 'cache' => 0));
|
||||
$updateinfo .= $this->single_button($fetchurl, get_string('checkforupdates', 'core_plugin'));
|
||||
if ($fetch) {
|
||||
@@ -912,7 +913,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
*/
|
||||
protected function moodle_available_update_info(\core\update\info $updateinfo) {
|
||||
|
||||
$boxclasses = 'moodleupdateinfo';
|
||||
$boxclasses = 'moodleupdateinfo mb-2';
|
||||
$info = array();
|
||||
|
||||
if (isset($updateinfo->release)) {
|
||||
@@ -932,7 +933,8 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
}
|
||||
|
||||
if (isset($updateinfo->download)) {
|
||||
$info[] = html_writer::link($updateinfo->download, get_string('download'), array('class' => 'info download'));
|
||||
$info[] = html_writer::link($updateinfo->download, get_string('download'),
|
||||
array('class' => 'info download btn btn-secondary'));
|
||||
}
|
||||
|
||||
if (isset($updateinfo->url)) {
|
||||
@@ -940,9 +942,9 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
array('class' => 'info more'));
|
||||
}
|
||||
|
||||
$box = $this->output->box_start($boxclasses);
|
||||
$box .= $this->output->box(implode(html_writer::tag('span', ' ', array('class' => 'separator')), $info), '');
|
||||
$box .= $this->output->box_end();
|
||||
$box = $this->output->container_start($boxclasses);
|
||||
$box .= $this->output->container(implode(html_writer::tag('span', ' | ', array('class' => 'separator')), $info), '');
|
||||
$box .= $this->output->container_end();
|
||||
|
||||
return $box;
|
||||
}
|
||||
@@ -1054,7 +1056,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
$displayname = new html_table_cell(
|
||||
$icon.
|
||||
html_writer::span($plugin->displayname, 'pluginname').
|
||||
html_writer::div($plugin->get_dir(), 'plugindir')
|
||||
html_writer::div($plugin->get_dir(), 'plugindir text-muted small')
|
||||
);
|
||||
|
||||
$versiondb = new html_table_cell($plugin->versiondb);
|
||||
@@ -1089,7 +1091,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
break;
|
||||
case core_plugin_manager::PLUGIN_STATUS_NODB:
|
||||
case core_plugin_manager::PLUGIN_STATUS_UPTODATE:
|
||||
$statusclass .= $dependenciesok ? '' : 'badge-warning';
|
||||
$statusclass .= $dependenciesok ? 'badge-light' : 'badge-warning';
|
||||
break;
|
||||
}
|
||||
$status = html_writer::span(get_string('status_' . $statuscode, 'core_plugin'), $statusclass);
|
||||
@@ -1099,7 +1101,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
new moodle_url($this->page->url, array('abortinstall' => $plugin->component)),
|
||||
get_string('cancelinstallone', 'core_plugin'),
|
||||
'post',
|
||||
array('class' => 'actionbutton cancelinstallone')
|
||||
array('class' => 'actionbutton cancelinstallone d-block mt-1')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1108,7 +1110,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
new moodle_url($this->page->url, array('abortupgrade' => $plugin->component)),
|
||||
get_string('cancelupgradeone', 'core_plugin'),
|
||||
'post',
|
||||
array('class' => 'actionbutton cancelupgradeone')
|
||||
array('class' => 'actionbutton cancelupgradeone d-block mt-1')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1178,7 +1180,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
}
|
||||
}
|
||||
|
||||
$out .= $this->output->container_start('actions');
|
||||
$out .= $this->output->container_start('actions mb-2');
|
||||
|
||||
$installableupdates = $pluginman->filter_installable($pluginman->available_updates());
|
||||
if ($installableupdates) {
|
||||
@@ -1186,7 +1188,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
new moodle_url($this->page->url, array('installupdatex' => 1)),
|
||||
get_string('updateavailableinstallall', 'core_admin', count($installableupdates)),
|
||||
'post',
|
||||
array('class' => 'singlebutton updateavailableinstallall')
|
||||
array('class' => 'singlebutton updateavailableinstallall mr-1')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1195,7 +1197,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
new moodle_url($this->page->url, array('abortinstallx' => 1)),
|
||||
get_string('cancelinstallall', 'core_plugin', count($installabortable)),
|
||||
'post',
|
||||
array('class' => 'singlebutton cancelinstallall')
|
||||
array('class' => 'singlebutton cancelinstallall mr-1')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1204,15 +1206,17 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
new moodle_url($this->page->url, array('abortupgradex' => 1)),
|
||||
get_string('cancelupgradeall', 'core_plugin', count($upgradeabortable)),
|
||||
'post',
|
||||
array('class' => 'singlebutton cancelupgradeall')
|
||||
array('class' => 'singlebutton cancelupgradeall mr-1')
|
||||
);
|
||||
}
|
||||
|
||||
$out .= html_writer::div(html_writer::link(new moodle_url($this->page->url, array('showallplugins' => 0)),
|
||||
get_string('plugincheckattention', 'core_plugin')).' '.html_writer::span($sumattention, 'badge'));
|
||||
get_string('plugincheckattention', 'core_plugin')).' '.html_writer::span($sumattention, 'badge badge-light'),
|
||||
'btn btn-link mr-1');
|
||||
|
||||
$out .= html_writer::div(html_writer::link(new moodle_url($this->page->url, array('showallplugins' => 1)),
|
||||
get_string('plugincheckall', 'core_plugin')).' '.html_writer::span($sumtotal, 'badge'));
|
||||
get_string('plugincheckall', 'core_plugin')).' '.html_writer::span($sumtotal, 'badge badge-light'),
|
||||
'btn btn-link mr-1');
|
||||
|
||||
$out .= $this->output->container_end(); // End of .actions container.
|
||||
$out .= $this->output->container_end(); // End of #plugins-check-info container.
|
||||
@@ -1280,12 +1284,13 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
}
|
||||
}
|
||||
|
||||
$out = $this->output->container_start('plugins-check-dependencies');
|
||||
$out = $this->output->container_start('plugins-check-dependencies mb-4');
|
||||
|
||||
if ($unavailable or $unknown) {
|
||||
$out .= $this->output->heading(get_string('misdepsunavail', 'core_plugin'));
|
||||
if ($unknown) {
|
||||
$out .= $this->output->notification(get_string('misdepsunknownlist', 'core_plugin', implode($unknown, ', ')));
|
||||
$out .= $this->output->render((new \core\output\notification(get_string('misdepsunknownlist', 'core_plugin',
|
||||
implode(', ', $unknown))))->set_show_closebutton(false));
|
||||
}
|
||||
if ($unavailable) {
|
||||
$unavailablelist = array();
|
||||
@@ -1299,18 +1304,18 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
}
|
||||
$unavailablelist[] = $unavailablelistitem;
|
||||
}
|
||||
$out .= $this->output->notification(get_string('misdepsunavaillist', 'core_plugin',
|
||||
implode($unavailablelist, ', ')));
|
||||
$out .= $this->output->render((new \core\output\notification(get_string('misdepsunavaillist', 'core_plugin',
|
||||
implode(', ', $unavailablelist))))->set_show_closebutton(false));
|
||||
}
|
||||
$out .= $this->output->container_start('plugins-check-dependencies-actions');
|
||||
$out .= $this->output->container_start('plugins-check-dependencies-actions mb-4');
|
||||
$out .= ' '.html_writer::link(new moodle_url('/admin/tool/installaddon/'),
|
||||
get_string('dependencyuploadmissing', 'core_plugin'));
|
||||
get_string('dependencyuploadmissing', 'core_plugin'), array('class' => 'btn btn-secondary'));
|
||||
$out .= $this->output->container_end(); // End of .plugins-check-dependencies-actions container.
|
||||
}
|
||||
|
||||
if ($available) {
|
||||
$out .= $this->output->heading(get_string('misdepsavail', 'core_plugin'));
|
||||
$out .= $this->output->container_start('plugins-check-dependencies-actions');
|
||||
$out .= $this->output->container_start('plugins-check-dependencies-actions mb-2');
|
||||
|
||||
$installable = $pluginman->filter_installable($available);
|
||||
if ($installable) {
|
||||
@@ -1318,12 +1323,13 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
new moodle_url($this->page->url, array('installdepx' => 1)),
|
||||
get_string('dependencyinstallmissing', 'core_plugin', count($installable)),
|
||||
'post',
|
||||
array('class' => 'singlebutton dependencyinstallmissing')
|
||||
array('class' => 'singlebutton dependencyinstallmissing d-inline-block mr-1')
|
||||
);
|
||||
}
|
||||
|
||||
$out .= html_writer::div(html_writer::link(new moodle_url('/admin/tool/installaddon/'),
|
||||
get_string('dependencyuploadmissing', 'core_plugin')), 'dependencyuploadmissing');
|
||||
get_string('dependencyuploadmissing', 'core_plugin'), array('class' => 'btn btn-link')),
|
||||
'dependencyuploadmissing d-inline-block mr-1');
|
||||
|
||||
$out .= $this->output->container_end(); // End of .plugins-check-dependencies-actions container.
|
||||
|
||||
@@ -1364,7 +1370,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
if ($CFG->branch == str_replace('.', '', $moodle->release)) {
|
||||
$supportedmoodles[] = html_writer::span($moodle->release, 'badge badge-success');
|
||||
} else {
|
||||
$supportedmoodles[] = html_writer::span($moodle->release, 'label');
|
||||
$supportedmoodles[] = html_writer::span($moodle->release, 'badge badge-light');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1378,7 +1384,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
}
|
||||
$info = html_writer::div(
|
||||
get_string('requiredby', 'core_plugin', implode(', ', $requriedby)),
|
||||
'requiredby'
|
||||
'requiredby mb-1'
|
||||
);
|
||||
} else {
|
||||
$info = '';
|
||||
@@ -1389,35 +1395,36 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
$info .= html_writer::div(
|
||||
html_writer::link('https://moodle.org/plugins/view.php?plugin='.$plugin->component,
|
||||
get_string('misdepinfoplugin', 'core_plugin')),
|
||||
'misdepinfoplugin'
|
||||
'misdepinfoplugin d-inline-block mr-3 mb-1'
|
||||
);
|
||||
|
||||
$info .= html_writer::div(
|
||||
html_writer::link('https://moodle.org/plugins/pluginversion.php?id='.$plugin->version->id,
|
||||
get_string('misdepinfoversion', 'core_plugin')),
|
||||
'misdepinfoversion'
|
||||
'misdepinfoversion d-inline-block mr-3 mb-1'
|
||||
);
|
||||
|
||||
$info .= html_writer::div(html_writer::link($plugin->version->downloadurl, get_string('download')), 'misdepdownload');
|
||||
$info .= html_writer::div(html_writer::link($plugin->version->downloadurl, get_string('download')),
|
||||
'misdepdownload d-inline-block mr-3 mb-1');
|
||||
|
||||
if ($pluginman->is_remote_plugin_installable($plugin->component, $plugin->version->version, $reason)) {
|
||||
$info .= $this->output->single_button(
|
||||
new moodle_url($this->page->url, array('installdep' => $plugin->component)),
|
||||
get_string('dependencyinstall', 'core_plugin'),
|
||||
'post',
|
||||
array('class' => 'singlebutton dependencyinstall')
|
||||
array('class' => 'singlebutton dependencyinstall mr-3 mb-1')
|
||||
);
|
||||
} else {
|
||||
$reasonhelp = $this->info_remote_plugin_not_installable($reason);
|
||||
if ($reasonhelp) {
|
||||
$info .= html_writer::div($reasonhelp, 'reasonhelp dependencyinstall');
|
||||
$info .= html_writer::div($reasonhelp, 'reasonhelp dependencyinstall d-inline-block mr-3 mb-1');
|
||||
}
|
||||
}
|
||||
|
||||
$info .= $this->output->container_end(); // End of .actions container.
|
||||
|
||||
$table->data[] = array(
|
||||
html_writer::div($plugin->name, 'name').' '.html_writer::div($plugin->component, 'component'),
|
||||
html_writer::div($plugin->name, 'name').' '.html_writer::div($plugin->component, 'component text-muted small'),
|
||||
$plugin->version->release,
|
||||
$plugin->version->version,
|
||||
implode($supportedmoodles, ' '),
|
||||
@@ -1463,7 +1470,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
foreach ($pluginman->resolve_requirements($plugin, $version) as $reqname => $reqinfo) {
|
||||
if ($reqname === 'core') {
|
||||
if ($reqinfo->status == $pluginman::REQUIREMENT_STATUS_OK) {
|
||||
$class = 'requires-ok';
|
||||
$class = 'requires-ok text-muted';
|
||||
$label = '';
|
||||
} else {
|
||||
$class = 'requires-failed';
|
||||
@@ -1480,7 +1487,7 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
|
||||
if ($reqinfo->status == $pluginman::REQUIREMENT_STATUS_OK) {
|
||||
$label = '';
|
||||
$class = 'requires-ok';
|
||||
$class = 'requires-ok text-muted';
|
||||
|
||||
} else if ($reqinfo->status == $pluginman::REQUIREMENT_STATUS_MISSING) {
|
||||
if ($reqinfo->availability == $pluginman::REQUIREMENT_AVAILABLE) {
|
||||
@@ -1534,13 +1541,14 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
return '';
|
||||
}
|
||||
|
||||
$out = html_writer::tag('ul', implode("\n", $requires));
|
||||
$out = html_writer::tag('ul', implode("\n", $requires), array('class' => 'm-0'));
|
||||
|
||||
if ($displayuploadlink) {
|
||||
$out .= html_writer::div(
|
||||
html_writer::link(
|
||||
new moodle_url('/admin/tool/installaddon/'),
|
||||
get_string('dependencyuploadmissing', 'core_plugin')
|
||||
get_string('dependencyuploadmissing', 'core_plugin'),
|
||||
array('class' => 'btn btn-secondary btn-sm m-1')
|
||||
),
|
||||
'dependencyuploadmissing'
|
||||
);
|
||||
@@ -1550,7 +1558,8 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
$out .= html_writer::div(
|
||||
html_writer::link(
|
||||
new moodle_url($this->page->url, array('sesskey' => sesskey(), 'fetchupdates' => 1)),
|
||||
get_string('checkforupdates', 'core_plugin')
|
||||
get_string('checkforupdates', 'core_plugin'),
|
||||
array('class' => 'btn btn-secondary btn-sm m-1')
|
||||
),
|
||||
'checkforupdates'
|
||||
);
|
||||
|
||||
@@ -97,7 +97,10 @@ $options = array(
|
||||
GETREMOTEADDR_SKIP_HTTP_CLIENT_IP => 'HTTP_X_FORWARDED_FOR, REMOTE_ADDR',
|
||||
GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR => 'HTTP_CLIENT, REMOTE_ADDR',
|
||||
GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR|GETREMOTEADDR_SKIP_HTTP_CLIENT_IP => 'REMOTE_ADDR');
|
||||
$temp->add(new admin_setting_configselect('getremoteaddrconf', new lang_string('getremoteaddrconf', 'admin'), new lang_string('configgetremoteaddrconf', 'admin'), 0, $options));
|
||||
$temp->add(new admin_setting_configselect('getremoteaddrconf', new lang_string('getremoteaddrconf', 'admin'),
|
||||
new lang_string('configgetremoteaddrconf', 'admin'),
|
||||
GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR|GETREMOTEADDR_SKIP_HTTP_CLIENT_IP, $options));
|
||||
$temp->add(new admin_setting_configtext('reverseproxyignore', new lang_string('reverseproxyignore', 'admin'), new lang_string('configreverseproxyignore', 'admin'), ''));
|
||||
|
||||
$temp->add(new admin_setting_heading('webproxy', new lang_string('webproxy', 'admin'), new lang_string('webproxyinfo', 'admin')));
|
||||
$temp->add(new admin_setting_configtext('proxyhost', new lang_string('proxyhost', 'admin'), new lang_string('configproxyhost', 'admin'), '', PARAM_HOST));
|
||||
|
||||
@@ -46,7 +46,8 @@ if ($data) {
|
||||
$emailuser->email = $data->recipient;
|
||||
$emailuser->id = -99;
|
||||
|
||||
$subject = get_string('testoutgoingmailconf_subject', 'admin', $SITE->fullname);
|
||||
$subject = get_string('testoutgoingmailconf_subject', 'admin',
|
||||
format_string($SITE->fullname, true, ['context' => context_system::instance()]));
|
||||
$messagetext = get_string('testoutgoingmailconf_message', 'admin');
|
||||
|
||||
// Manage Moodle debugging options.
|
||||
|
||||
@@ -32,7 +32,7 @@ require_once($CFG->libdir . '/behat/classes/behat_config_manager.php');
|
||||
// systems, but let's allow room for expansion.
|
||||
core_php_time_limit::raise(300);
|
||||
|
||||
$filter = optional_param('filter', '', PARAM_ALPHANUMEXT);
|
||||
$filter = optional_param('filter', '', PARAM_NOTAGS);
|
||||
$type = optional_param('type', false, PARAM_ALPHAEXT);
|
||||
$component = optional_param('component', '', PARAM_ALPHAEXT);
|
||||
|
||||
|
||||
@@ -55,13 +55,13 @@ class tool_behat {
|
||||
// The loaded steps depends on the component specified.
|
||||
behat_config_manager::update_config_file($component, false);
|
||||
|
||||
// The Moodle\BehatExtension\HelpPrinter\MoodleDefinitionsPrinter will parse this search format.
|
||||
// The Moodle\BehatExtension\Definition\Printer\ConsoleDefinitionInformationPrinter will parse this search format.
|
||||
if ($type) {
|
||||
$filter .= '&&' . $type;
|
||||
}
|
||||
|
||||
if ($filter) {
|
||||
$filteroption = ' -d "' . $filter . '"';
|
||||
$filteroption = ' -d ' . escapeshellarg($filter);
|
||||
} else {
|
||||
$filteroption = ' -di';
|
||||
}
|
||||
|
||||
@@ -44,13 +44,9 @@ class tool_behat_renderer extends plugin_renderer_base {
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/behat/classes/behat_selectors.php');
|
||||
|
||||
$html = $this->generic_info();
|
||||
|
||||
// Form.
|
||||
ob_start();
|
||||
$form->display();
|
||||
$html .= ob_get_contents();
|
||||
ob_end_clean();
|
||||
$html = $this->output->header();
|
||||
$html .= $this->output->heading(get_string('pluginname', 'tool_behat'));
|
||||
$html .= $form->render();
|
||||
|
||||
if (empty($stepsdefinitions)) {
|
||||
$stepsdefinitions = get_string('nostepsdefinitions', 'tool_behat');
|
||||
@@ -128,7 +124,9 @@ class tool_behat_renderer extends plugin_renderer_base {
|
||||
*/
|
||||
public function render_error($msg) {
|
||||
|
||||
$html = $this->generic_info();
|
||||
$html = $this->output->header();
|
||||
$html .= $this->output->heading(get_string('pluginname', 'tool_behat'));
|
||||
$html .= $this->generic_info();
|
||||
|
||||
$a = new stdClass();
|
||||
$a->errormsg = $msg;
|
||||
@@ -153,13 +151,7 @@ class tool_behat_renderer extends plugin_renderer_base {
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function generic_info() {
|
||||
|
||||
$title = get_string('pluginname', 'tool_behat');
|
||||
|
||||
// Header.
|
||||
$html = $this->output->header();
|
||||
$html .= $this->output->heading($title);
|
||||
public function generic_info() {
|
||||
|
||||
// Info.
|
||||
$installurl = behat_command::DOCS_URL;
|
||||
@@ -175,8 +167,7 @@ class tool_behat_renderer extends plugin_renderer_base {
|
||||
);
|
||||
|
||||
// List of steps.
|
||||
$html .= $this->output->box_start();
|
||||
$html .= html_writer::tag('h3', get_string('infoheading', 'tool_behat'));
|
||||
$html = $this->output->box_start();
|
||||
$html .= html_writer::tag('div', get_string('aim', 'tool_behat'));
|
||||
$html .= html_writer::start_tag('div');
|
||||
$html .= html_writer::start_tag('ul');
|
||||
|
||||
@@ -40,8 +40,14 @@ class steps_definitions_form extends moodleform {
|
||||
* @return void
|
||||
*/
|
||||
public function definition() {
|
||||
global $PAGE;
|
||||
|
||||
$mform = $this->_form;
|
||||
$output = $PAGE->get_renderer('tool_behat');
|
||||
|
||||
$mform->addElement('header', 'info', get_string('infoheading', 'tool_behat'));
|
||||
$mform->setExpanded('info', false);
|
||||
$mform->addElement('html', $output->generic_info());
|
||||
|
||||
$mform->addElement('header', 'filters', get_string('stepsdefinitionsfilters', 'tool_behat'));
|
||||
|
||||
|
||||
+20
-14
@@ -1,25 +1,31 @@
|
||||
.steps-definitions {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: #bbb;
|
||||
padding: 5px;
|
||||
margin: auto;
|
||||
width: 50%;
|
||||
#page-admin-tool-behat-index .steps-definitions {
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
.steps-definitions .step {
|
||||
margin: 10px 0 10px 0;
|
||||
#page-admin-tool-behat-index .steps-definitions .step {
|
||||
margin: 1rem 0 0 0;
|
||||
border: 1px solid #eee;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.steps-definitions .stepdescription {
|
||||
color: #bf8c12;
|
||||
#page-admin-tool-behat-index .steps-definitions .stepdescription {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.steps-definitions .steptype {
|
||||
#page-admin-tool-behat-index .steps-definitions .stepcontent {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
#page-admin-tool-behat-index .steps-definitions .steptype {
|
||||
color: #1467a6;
|
||||
margin-right: 5px;
|
||||
margin-right: 1ex;
|
||||
}
|
||||
|
||||
.steps-definitions .stepregex {
|
||||
#page-admin-tool-behat-index .steps-definitions .stepapipath {
|
||||
font-family: monospace;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
#page-admin-tool-behat-index .steps-definitions .stepregex {
|
||||
color: #060;
|
||||
}
|
||||
|
||||
@@ -26,3 +26,11 @@ Feature: List the system steps definitions
|
||||
Given I set the field "Contains" to "homepage"
|
||||
When I press "Filter"
|
||||
Then I should see "Opens Moodle homepage."
|
||||
|
||||
@javascript
|
||||
Scenario: Filtering by the multiple words pattern
|
||||
Given I set the field "Contains" to "should exist"
|
||||
When I press "Filter"
|
||||
Then I should not see "There aren't steps definitions matching this filter"
|
||||
And I should see "Checks the provided element and selector type exists in the current page."
|
||||
And I should see "Checks that an element and selector type exists in another element and selector type on the current page."
|
||||
|
||||
@@ -75,6 +75,7 @@ class tool_capability_renderer extends plugin_renderer_base {
|
||||
* @return string
|
||||
*/
|
||||
public function capability_comparison_table(array $capabilities, $contextid, array $roles) {
|
||||
static $capabilitycontexts = array();
|
||||
|
||||
$strpermissions = $this->get_permission_strings();
|
||||
$permissionclasses = $this->get_permission_classes();
|
||||
@@ -92,8 +93,13 @@ class tool_capability_renderer extends plugin_renderer_base {
|
||||
}
|
||||
$table->data = array();
|
||||
|
||||
$childcontextsids = [];
|
||||
foreach ($capabilities as $capability) {
|
||||
$contexts = tool_capability_calculate_role_data($capability, $roles);
|
||||
if (empty($capabilitycontexts[$capability])) {
|
||||
$capabilitycontexts[$capability] = tool_capability_calculate_role_data($capability, $roles);
|
||||
}
|
||||
$contexts = $capabilitycontexts[$capability];
|
||||
|
||||
$captitle = new html_table_cell(get_capability_string($capability) . html_writer::span($capability));
|
||||
$captitle->header = true;
|
||||
|
||||
@@ -111,6 +117,10 @@ class tool_capability_renderer extends plugin_renderer_base {
|
||||
}
|
||||
|
||||
$table->data[] = $row;
|
||||
if (!empty($contexts[$contextid]->children)) {
|
||||
$childcontextsids = array_merge($childcontextsids, $contexts[$contextid]->children);
|
||||
$childcontextsids = array_unique($childcontextsids);
|
||||
}
|
||||
}
|
||||
|
||||
// Start the list item, and print the context name as a link to the place to make changes.
|
||||
@@ -127,9 +137,9 @@ class tool_capability_renderer extends plugin_renderer_base {
|
||||
$html = $this->output->heading(html_writer::link($url, $context->get_context_name(), ['title' => $title]), 3);
|
||||
$html .= html_writer::table($table);
|
||||
// If there are any child contexts, print them recursively.
|
||||
if (!empty($contexts[$contextid]->children)) {
|
||||
foreach ($contexts[$contextid]->children as $childcontextid) {
|
||||
$html .= $this->capability_comparison_table($capabilities, $childcontextid, $roles, true);
|
||||
if (!empty($childcontextsids)) {
|
||||
foreach ($childcontextsids as $childcontextid) {
|
||||
$html .= $this->capability_comparison_table($capabilities, $childcontextid, $roles);
|
||||
}
|
||||
}
|
||||
return $html;
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
@tool @tool_capability
|
||||
Feature: Show capabilities for multiple contexts
|
||||
In order to check roles capabilities
|
||||
As an admin
|
||||
I need to be able to see capability overrides on several contexts
|
||||
|
||||
Background:
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
| Course 2 | C2 | 0 |
|
||||
And the following "permission overrides" exist:
|
||||
| capability | permission | role | contextlevel | reference |
|
||||
| enrol/category:config | Allow | student | Course | C1 |
|
||||
| enrol/cohort:unenrol | Allow | student | Course | C2 |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Users > Permissions > Capability overview" in site administration
|
||||
|
||||
Scenario: Show capabilities table with one capability with overrides
|
||||
When I set the following fields to these values:
|
||||
| Capability: | enrol/category:config |
|
||||
| Roles: | Student |
|
||||
And I click on "Get the overview" "button"
|
||||
Then I should see "System"
|
||||
And I should see "Category: Miscellaneous"
|
||||
And I should see "Course: Course 1"
|
||||
And I should not see "Course: Course 2"
|
||||
|
||||
Scenario: Show capabilities table with one capability without overrides
|
||||
When I set the following fields to these values:
|
||||
| Capability: | enrol/cohort:config |
|
||||
| Roles: | Student |
|
||||
And I click on "Get the overview" "button"
|
||||
Then I should see "System"
|
||||
And I should not see "Category: Miscellaneous"
|
||||
And I should not see "Course: Course 1"
|
||||
And I should not see "Course: Course 2"
|
||||
|
||||
Scenario: Show capabilities table with two capabilities, 1st without overrides and 2nd with
|
||||
When I set the following fields to these values:
|
||||
| Capability: | enrol/category:synchronised, enrol/category:config |
|
||||
| Roles: | Student |
|
||||
And I click on "Get the overview" "button"
|
||||
Then I should see "System"
|
||||
And I should see "Category: Miscellaneous"
|
||||
And I should see "Course: Course 1"
|
||||
And I should not see "Course: Course 2"
|
||||
|
||||
Scenario: Show capabilities table with two capabilities, 1st with overrides and 2nd without
|
||||
When I set the following fields to these values:
|
||||
| Capability: | enrol/category:config, enrol/cohort:config |
|
||||
| Roles: | Student |
|
||||
And I click on "Get the overview" "button"
|
||||
Then I should see "System"
|
||||
And I should see "Category: Miscellaneous"
|
||||
And I should see "Course: Course 1"
|
||||
And I should not see "Course: Course 2"
|
||||
|
||||
Scenario: Show capabilities table with two capabilities, none with overrides
|
||||
When I set the following fields to these values:
|
||||
| Capability: | enrol/category:synchronised, enrol/cohort:config |
|
||||
| Roles: | Student |
|
||||
And I click on "Get the overview" "button"
|
||||
Then I should see "System"
|
||||
And I should not see "Category: Miscellaneous"
|
||||
And I should not see "Course: Course 1"
|
||||
And I should not see "Course: Course 2"
|
||||
|
||||
Scenario: Show capabilities table with capability with override and no role selected
|
||||
When I set the following fields to these values:
|
||||
| Capability: | enrol/category:config |
|
||||
| Roles: | |
|
||||
And I click on "Get the overview" "button"
|
||||
Then I should see "System"
|
||||
And I should see "Category: Miscellaneous"
|
||||
And I should see "Course: Course 1"
|
||||
And I should not see "Course: Course 2"
|
||||
|
||||
Scenario: Show capabilities table with capability without override and no role selected
|
||||
When I set the following fields to these values:
|
||||
| Capability: | enrol/cohort:config |
|
||||
| Roles: | |
|
||||
And I click on "Get the overview" "button"
|
||||
Then I should see "System"
|
||||
And I should not see "Category: Miscellaneous"
|
||||
And I should not see "Course: Course 1"
|
||||
And I should not see "Course: Course 2"
|
||||
|
||||
Scenario: Show capabilities table with capability with two overrides on different contexts
|
||||
When I set the following fields to these values:
|
||||
| Capability: | enrol/category:config, enrol/cohort:unenrol |
|
||||
| Roles: | |
|
||||
And I click on "Get the overview" "button"
|
||||
Then I should see "System"
|
||||
And I should see "Category: Miscellaneous"
|
||||
And I should see "Course: Course 1"
|
||||
And I should see "Course: Course 2"
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Plugin upgrade code
|
||||
*
|
||||
* @package tool_cohortroles
|
||||
* @copyright 2020 Paul Holden <paulh@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Function to upgrade tool_cohortroles.
|
||||
*
|
||||
* @param int $oldversion the version we are upgrading from
|
||||
* @return bool result
|
||||
*/
|
||||
function xmldb_tool_cohortroles_upgrade($oldversion) {
|
||||
global $DB;
|
||||
|
||||
if ($oldversion < 2019052001) {
|
||||
// Delete any tool_cohortroles mappings for roles which no longer exist.
|
||||
$DB->delete_records_select('tool_cohortroles', 'roleid NOT IN (SELECT id FROM {role})');
|
||||
|
||||
// Cohortroles savepoint reached.
|
||||
upgrade_plugin_savepoint(true, 2019052001, 'tool', 'cohortroles');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -25,7 +25,7 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
$plugin->version = 2019052000; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->version = 2019052001; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->requires = 2019051100; // Requires this Moodle version.
|
||||
$plugin->component = 'tool_cohortroles'; // Full name of the plugin (used for diagnostics).
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events","core/templates"],function(a,b,c,d,e,f,g){function h(a){var e={methodname:"tool_dataprivacy_contact_dpo",args:{message:a}},f="success";b.call([e])[0].then(function(a){return a.result?d.get_string("requestsubmitted","tool_dataprivacy"):(f="error",a.warnings.join("<br>"))}).done(function(a){c.addNotification({message:a,type:f})}).fail(c.exception)}var i={CANCEL_REQUEST:'[data-action="cancel"]',CONTACT_DPO:'[data-action="contactdpo"]'},j=function(){this.registerEvents()};return j.prototype.registerEvents=function(){a(i.CANCEL_REQUEST).click(function(g){g.preventDefault();var h=a(this).data("requestid"),i=[{key:"cancelrequest",component:"tool_dataprivacy"},{key:"cancelrequestconfirmation",component:"tool_dataprivacy"}];d.get_strings(i).then(function(a){var d=a[0],g=a[1];return e.create({title:d,body:g,type:e.types.SAVE_CANCEL}).then(function(a){return a.setSaveButtonText(d),a.getRoot().on(f.save,function(){var a={requestid:h},d={methodname:"tool_dataprivacy_cancel_data_request",args:a};b.call([d])[0].done(function(a){a.result?window.location.reload():c.addNotification({message:a.warnings[0].message,type:"error"})}).fail(c.exception)}),a.getRoot().on(f.hidden,function(){a.destroy()}),a})}).done(function(a){a.show()}).fail(c.exception)}),a(i.CONTACT_DPO).click(function(b){b.preventDefault();var i=a(this).data("replytoemail"),j=[{key:"contactdataprotectionofficer",component:"tool_dataprivacy"},{key:"send",component:"tool_dataprivacy"}],k="";d.get_strings(j).then(function(a){var b=a[0];k=a[1];var c={replytoemail:i};return e.create({title:b,body:g.render("tool_dataprivacy/contact_dpo",c),type:e.types.SAVE_CANCEL,large:!0})}).done(function(b){b.setSaveButtonText(k),b.getRoot().on(f.save,function(b){var c=a("#message").val().trim();0===c.length?(b.preventDefault(),a('[data-region="messageinput"]').addClass("has-danger notifyproblem"),a("#id_error_message").removeAttr("hidden")):h(c)}),b.getRoot().on(f.hidden,function(){b.destroy()}),b.show()}).fail(c.exception)})},{init:function(){return new j}}});
|
||||
define(["jquery","core/ajax","core/notification","core/str","core/modal_factory","core/modal_events","core/templates","core/pending"],function(a,b,c,d,e,f,g,h){function i(a){var e={methodname:"tool_dataprivacy_contact_dpo",args:{message:a}},f="success";b.call([e])[0].then(function(a){return a.result?d.get_string("requestsubmitted","tool_dataprivacy"):(f="error",a.warnings.join("<br>"))}).done(function(a){c.addNotification({message:a,type:f})}).fail(c.exception)}var j={CANCEL_REQUEST:'[data-action="cancel"]',CONTACT_DPO:'[data-action="contactdpo"]'},k=function(){this.registerEvents()};return k.prototype.registerEvents=function(){a(j.CANCEL_REQUEST).click(function(g){g.preventDefault();var h=a(this).data("requestid"),i=[{key:"cancelrequest",component:"tool_dataprivacy"},{key:"cancelrequestconfirmation",component:"tool_dataprivacy"}];d.get_strings(i).then(function(a){var d=a[0],g=a[1];return e.create({title:d,body:g,type:e.types.SAVE_CANCEL}).then(function(a){return a.setSaveButtonText(d),a.getRoot().on(f.save,function(){var a={requestid:h},d={methodname:"tool_dataprivacy_cancel_data_request",args:a};b.call([d])[0].done(function(a){a.result?window.location.reload():c.addNotification({message:a.warnings[0].message,type:"error"})}).fail(c.exception)}),a.getRoot().on(f.hidden,function(){a.destroy()}),a})}).done(function(a){a.show()}).fail(c.exception)}),a(j.CONTACT_DPO).click(function(b){var j=new h("dataprivacy/crud:initModal:contactdpo");b.preventDefault();var k=a(this).data("replytoemail"),l=[{key:"contactdataprotectionofficer",component:"tool_dataprivacy"},{key:"send",component:"tool_dataprivacy"}],m="";d.get_strings(l).then(function(a){var b=a[0];m=a[1];var c={replytoemail:k};return e.create({title:b,body:g.render("tool_dataprivacy/contact_dpo",c),type:e.types.SAVE_CANCEL,large:!0})}).then(function(b){b.setSaveButtonText(m),b.show(),b.getRoot().on(f.save,function(b){var c=a("#message").val().trim();0===c.length?(b.preventDefault(),a('[data-region="messageinput"]').addClass("has-danger notifyproblem"),a("#id_error_message").removeAttr("hidden")):i(c)}),b.getRoot().on(f.hidden,function(){b.destroy()})}).then(j.resolve)["catch"](c.exception)})},{init:function(){return new k}}});
|
||||
@@ -28,8 +28,9 @@ define([
|
||||
'core/str',
|
||||
'core/modal_factory',
|
||||
'core/modal_events',
|
||||
'core/templates'],
|
||||
function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates) {
|
||||
'core/templates',
|
||||
'core/pending'],
|
||||
function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates, Pending) {
|
||||
|
||||
/**
|
||||
* List of action selectors.
|
||||
@@ -118,6 +119,7 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates) {
|
||||
});
|
||||
|
||||
$(ACTIONS.CONTACT_DPO).click(function(e) {
|
||||
var pendingPromise = new Pending('dataprivacy/crud:initModal:contactdpo');
|
||||
e.preventDefault();
|
||||
|
||||
var replyToEmail = $(this).data('replytoemail');
|
||||
@@ -146,9 +148,12 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates) {
|
||||
type: ModalFactory.types.SAVE_CANCEL,
|
||||
large: true
|
||||
});
|
||||
}).done(function(modal) {
|
||||
}).then(function(modal) {
|
||||
modal.setSaveButtonText(sendButtonText);
|
||||
|
||||
// Show the modal!
|
||||
modal.show();
|
||||
|
||||
// Handle send event.
|
||||
modal.getRoot().on(ModalEvents.save, function(e) {
|
||||
var message = $('#message').val().trim();
|
||||
@@ -169,9 +174,9 @@ function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates) {
|
||||
modal.destroy();
|
||||
});
|
||||
|
||||
// Show the modal!
|
||||
modal.show();
|
||||
}).fail(Notification.exception);
|
||||
return;
|
||||
}).then(pendingPromise.resolve)
|
||||
.catch(Notification.exception);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -45,13 +45,22 @@ if ($manage) {
|
||||
$context = context_system::instance();
|
||||
// Make sure the user has the proper capability.
|
||||
require_capability('tool/dataprivacy:managedatarequests', $context);
|
||||
navigation_node::override_active_url($returnurl);
|
||||
} else {
|
||||
// For the case where a user makes request for themselves (or for their children if they are the parent).
|
||||
$returnurl = new moodle_url($CFG->wwwroot . '/admin/tool/dataprivacy/mydatarequests.php');
|
||||
$context = context_user::instance($USER->id);
|
||||
}
|
||||
|
||||
$PAGE->set_context($context);
|
||||
|
||||
if (!$manage && $profilenode = $PAGE->settingsnav->find('myprofile', null)) {
|
||||
$profilenode->make_active();
|
||||
}
|
||||
|
||||
$title = get_string('createnewdatarequest', 'tool_dataprivacy');
|
||||
$PAGE->navbar->add($title);
|
||||
|
||||
// If contactdataprotectionofficer is disabled, send the user back to the profile page, or the privacy policy page.
|
||||
// That is, unless you have sufficient capabilities to perform this on behalf of a user.
|
||||
if (!$manage && !\tool_dataprivacy\api::can_contact_dpo()) {
|
||||
@@ -100,7 +109,6 @@ if ($data = $mform->get_data()) {
|
||||
redirect($returnurl, $redirectmessage);
|
||||
}
|
||||
|
||||
$title = get_string('createnewdatarequest', 'tool_dataprivacy');
|
||||
$PAGE->set_heading($SITE->fullname);
|
||||
$PAGE->set_title($title);
|
||||
echo $OUTPUT->header();
|
||||
|
||||
@@ -42,6 +42,13 @@ if (isguestuser()) {
|
||||
$usercontext = context_user::instance($USER->id);
|
||||
$PAGE->set_context($usercontext);
|
||||
|
||||
if ($profilenode = $PAGE->settingsnav->find('myprofile', null)) {
|
||||
$profilenode->make_active();
|
||||
}
|
||||
|
||||
$title = get_string('datarequests', 'tool_dataprivacy');
|
||||
$PAGE->navbar->add($title);
|
||||
|
||||
// Return URL.
|
||||
$params = ['id' => $USER->id];
|
||||
if ($courseid) {
|
||||
@@ -49,7 +56,6 @@ if ($courseid) {
|
||||
}
|
||||
$returnurl = new moodle_url('/user/profile.php', $params);
|
||||
|
||||
$title = get_string('datarequests', 'tool_dataprivacy');
|
||||
$PAGE->set_heading($title);
|
||||
$PAGE->set_title($title);
|
||||
echo $OUTPUT->header();
|
||||
|
||||
@@ -27,12 +27,10 @@ require_once('../../../config.php');
|
||||
$requestid = required_param('requestid', PARAM_INT);
|
||||
$confirm = optional_param('confirm', null, PARAM_INT);
|
||||
|
||||
$PAGE->set_url(new moodle_url('/admin/tool/dataprivacy/resubmitrequest.php', ['requestid' => $requestid]));
|
||||
$url = new moodle_url('/admin/tool/dataprivacy/resubmitrequest.php', ['requestid' => $requestid]);
|
||||
$title = get_string('resubmitrequestasnew', 'tool_dataprivacy');
|
||||
|
||||
require_login();
|
||||
|
||||
$PAGE->set_context(\context_system::instance());
|
||||
require_capability('tool/dataprivacy:managedatarequests', $PAGE->context);
|
||||
\tool_dataprivacy\page_helper::setup($url, $title, 'datarequests', 'tool/dataprivacy:managedatarequests');
|
||||
|
||||
$manageurl = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
|
||||
|
||||
@@ -53,10 +51,6 @@ if (null !== $confirm && confirm_sesskey()) {
|
||||
redirect($manageurl, get_string('resubmittedrequest', 'tool_dataprivacy', $stringparams));
|
||||
}
|
||||
|
||||
$heading = get_string('resubmitrequest', 'tool_dataprivacy', $stringparams);
|
||||
$PAGE->set_title($heading);
|
||||
$PAGE->set_heading($heading);
|
||||
|
||||
echo $OUTPUT->header();
|
||||
|
||||
$confirmstring = get_string('confirmrequestresubmit', 'tool_dataprivacy', $stringparams);
|
||||
|
||||
@@ -34,6 +34,13 @@ $PAGE->set_context($context);
|
||||
$PAGE->set_title($title);
|
||||
$PAGE->set_heading($SITE->fullname);
|
||||
|
||||
// If user is logged in, then use profile navigation in breadcrumbs.
|
||||
if ($profilenode = $PAGE->settingsnav->find('myprofile', null)) {
|
||||
$profilenode->make_active();
|
||||
}
|
||||
|
||||
$PAGE->navbar->add($title);
|
||||
|
||||
$output = $PAGE->get_renderer('tool_dataprivacy');
|
||||
echo $output->header();
|
||||
$summarypage = new \tool_dataprivacy\output\summary_page();
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Tests for the plugin privacy provider
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2020 Paul Holden <paulh@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use core_privacy\local\request\userlist;
|
||||
use core_privacy\local\request\writer;
|
||||
use core_privacy\tests\provider_testcase;
|
||||
use tool_dataprivacy\api;
|
||||
use tool_dataprivacy\local\helper;
|
||||
use tool_dataprivacy\privacy\provider;
|
||||
|
||||
/**
|
||||
* Privacy provider tests
|
||||
*
|
||||
* @package tool_dataprivacy
|
||||
* @copyright 2020 Paul Holden <paulh@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class tool_dataprivacy_privacy_provider_testcase extends provider_testcase {
|
||||
|
||||
/**
|
||||
* Test provider get_contexts_for_userid method
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_get_contexts_for_userid() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$context = context_user::instance($user->id);
|
||||
|
||||
// Returned context list should contain a single item.
|
||||
$contextlist = $this->get_contexts_for_userid($user->id, 'tool_dataprivacy');
|
||||
$this->assertCount(1, $contextlist);
|
||||
|
||||
// We should have the user context of our test user.
|
||||
$this->assertSame($context, $contextlist->current());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test provider get_users_in_context method
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_get_users_in_context() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$context = context_user::instance($user->id);
|
||||
|
||||
$userlist = new userlist($context, 'tool_dataprivacy');
|
||||
provider::get_users_in_context($userlist);
|
||||
|
||||
$this->assertEquals([$user->id], $userlist->get_userids());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test provider get_users_in_context method for a non-user context
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_get_users_in_context_non_user_context() {
|
||||
$context = context_system::instance();
|
||||
|
||||
$userlist = new userlist($context, 'tool_dataprivacy');
|
||||
provider::get_users_in_context($userlist);
|
||||
|
||||
$this->assertEmpty($userlist);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test provider export_user_data method
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_export_user_data() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$context = context_user::instance($user->id);
|
||||
|
||||
$this->setUser($user);
|
||||
|
||||
// Create an export request, approve it.
|
||||
$requestexport = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT,
|
||||
'Please export my stuff');
|
||||
api::update_request_status($requestexport->get('id'), api::DATAREQUEST_STATUS_APPROVED);
|
||||
|
||||
// Create a deletion request, reject it.
|
||||
$requestdelete = api::create_data_request($user->id, api::DATAREQUEST_TYPE_DELETE);
|
||||
api::update_request_status($requestdelete->get('id'), api::DATAREQUEST_STATUS_REJECTED, 0, 'Nope');
|
||||
|
||||
$this->export_context_data_for_user($user->id, $context, 'tool_dataprivacy');
|
||||
|
||||
/** @var \core_privacy\tests\request\content_writer $writer */
|
||||
$writer = writer::with_context($context);
|
||||
$this->assertTrue($writer->has_any_data());
|
||||
|
||||
/** @var stdClass[] $data */
|
||||
$data = (array) $writer->get_data([
|
||||
get_string('privacyandpolicies', 'admin'),
|
||||
get_string('datarequests', 'tool_dataprivacy'),
|
||||
]);
|
||||
|
||||
$this->assertCount(2, $data);
|
||||
|
||||
$strs = get_strings(['requesttypeexportshort', 'requesttypedeleteshort',
|
||||
'statusapproved', 'statusrejected', 'creationmanual'], 'tool_dataprivacy');
|
||||
|
||||
// First item is the approved export request.
|
||||
$this->assertEquals($strs->requesttypeexportshort, $data[0]->type);
|
||||
$this->assertEquals($strs->statusapproved, $data[0]->status);
|
||||
$this->assertEquals($strs->creationmanual, $data[0]->creationmethod);
|
||||
$this->assertEquals($requestexport->get('comments'), $data[0]->comments);
|
||||
$this->assertEmpty($data[0]->dpocomment);
|
||||
$this->assertNotEmpty($data[0]->timecreated);
|
||||
|
||||
// Next is the rejected deletion request.
|
||||
$this->assertEquals($strs->requesttypedeleteshort, $data[1]->type);
|
||||
$this->assertEquals($strs->statusrejected, $data[1]->status);
|
||||
$this->assertEquals($strs->creationmanual, $data[1]->creationmethod);
|
||||
$this->assertEmpty($data[1]->comments);
|
||||
$this->assertContains('Nope', $data[1]->dpocomment);
|
||||
$this->assertNotEmpty($data[1]->timecreated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test class export_user_preferences method
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_export_user_preferences() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$this->setUser($user);
|
||||
|
||||
// Set filters preference.
|
||||
$filters = [
|
||||
helper::FILTER_TYPE . ':' . api::DATAREQUEST_TYPE_EXPORT,
|
||||
helper::FILTER_STATUS . ':' . api::DATAREQUEST_STATUS_PENDING,
|
||||
];
|
||||
set_user_preference(helper::PREF_REQUEST_FILTERS, json_encode($filters), $user);
|
||||
|
||||
// Set paging preference.
|
||||
set_user_preference(helper::PREF_REQUEST_PERPAGE, 6, $user);
|
||||
|
||||
provider::export_user_preferences($user->id);
|
||||
|
||||
/** @var \core_privacy\tests\request\content_writer $writer */
|
||||
$writer = writer::with_context(context_system::instance());
|
||||
$this->assertTrue($writer->has_any_data());
|
||||
|
||||
/** @var stdClass[] $preferences */
|
||||
$preferences = (array) $writer->get_user_preferences('tool_dataprivacy');
|
||||
$this->assertCount(2, $preferences);
|
||||
|
||||
$this->assertEquals((object) [
|
||||
'value' => '1:1, 2:0',
|
||||
'description' => 'Type: Export, Status: Pending',
|
||||
], $preferences[helper::PREF_REQUEST_FILTERS]);
|
||||
|
||||
$this->assertEquals(6, $preferences[helper::PREF_REQUEST_PERPAGE]->value);
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
$string['classname'] = 'Class name';
|
||||
$string['component'] = 'Component';
|
||||
$string['configmessageinboundhost'] = 'The address of the server that Moodle should check mail against. To specify a non-default port, use [server]:[port], for example mail.example.com:587. If a port isn\'t specified, the default port for the type of mail server will be used.';
|
||||
$string['configmessageinboundhost'] = 'The address of the server that Moodle should check mail against. To specify a non-default port, use [server]:[port], for example mail.example.com:993. If a port isn\'t specified, the default port for the type of mail server will be used.';
|
||||
$string['defaultexpiration'] = 'Default address expiry period';
|
||||
$string['defaultexpiration_help'] = 'When an email address is generated by the handler, it can be set to automatically expire after a period of time, so that it can no longer be used. It is advisable to set an expiry period.';
|
||||
$string['description'] = 'Description';
|
||||
|
||||
@@ -49,6 +49,8 @@ class api {
|
||||
const LOGIN_VIA_EMBEDDED_BROWSER = 3;
|
||||
/** @var int seconds an auto-login key will expire. */
|
||||
const LOGIN_KEY_TTL = 60;
|
||||
/** @var string URL of the Moodle Apps Portal */
|
||||
const MOODLE_APPS_PORTAL_URL = 'https://apps.moodle.com';
|
||||
|
||||
/**
|
||||
* Returns a list of Moodle plugins supporting the mobile app.
|
||||
@@ -346,6 +348,7 @@ class api {
|
||||
*/
|
||||
public static function get_features_list() {
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/authlib.php');
|
||||
|
||||
$general = new lang_string('general');
|
||||
$mainmenu = new lang_string('mainmenu', 'tool_mobile');
|
||||
@@ -355,6 +358,7 @@ class api {
|
||||
$user = new lang_string('user');
|
||||
$files = new lang_string('files');
|
||||
$remoteaddons = new lang_string('remoteaddons', 'tool_mobile');
|
||||
$identityproviders = new lang_string('oauth2identityproviders', 'tool_mobile');
|
||||
|
||||
$availablemods = core_plugin_manager::instance()->get_plugins_of_type('mod');
|
||||
$coursemodules = array();
|
||||
@@ -421,6 +425,7 @@ class api {
|
||||
'NoDelegate_CoreTag' => new lang_string('tags'),
|
||||
'$mmLoginEmailSignup' => new lang_string('startsignup'),
|
||||
'NoDelegate_ResponsiveMainMenuItems' => new lang_string('responsivemainmenuitems', 'tool_mobile'),
|
||||
'NoDelegate_DarkMode' => new lang_string('darkmode', 'tool_mobile'),
|
||||
),
|
||||
"$mainmenu" => array(
|
||||
'$mmSideMenuDelegate_mmaFrontpage' => new lang_string('sitehome'),
|
||||
@@ -473,6 +478,31 @@ class api {
|
||||
$features["$remoteaddons"] = $remoteaddonslist;
|
||||
}
|
||||
|
||||
// Display OAuth 2 identity providers.
|
||||
if (is_enabled_auth('oauth2')) {
|
||||
$identityproviderslist = array();
|
||||
$idps = \auth_plugin_base::get_identity_providers(['oauth2']);
|
||||
|
||||
foreach ($idps as $idp) {
|
||||
// Only add identity providers that have an ID.
|
||||
$id = isset($idp['url']) ? $idp['url']->get_param('id') : null;
|
||||
if ($id != null) {
|
||||
$identityproviderslist['NoDelegate_IdentityProvider_' . $id] = $idp['name'];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($identityproviderslist)) {
|
||||
$features["$identityproviders"] = array();
|
||||
|
||||
if (count($identityproviderslist) > 1) {
|
||||
// Include an option to disable them all.
|
||||
$features["$identityproviders"]['NoDelegate_IdentityProviders'] = new lang_string('all');
|
||||
}
|
||||
|
||||
$features["$identityproviders"] = array_merge($features["$identityproviders"], $identityproviderslist);
|
||||
}
|
||||
}
|
||||
|
||||
return $features;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,12 +45,16 @@ $string['custommenuitems_desc'] = 'Additional items can be added to the app\'s m
|
||||
|
||||
Link-opening methods are: app (for linking to an activity supported by the app), inappbrowser (for opening a link in a browser without leaving the app), browser (for opening the link in the device default browser outside the app) and embedded (for displaying the link in an iframe in a new page in the app).
|
||||
|
||||
When items are missing a translation for a given language, they will use other languages as fallback unless "_only" is appended to the language code.
|
||||
|
||||
For example:
|
||||
<pre>
|
||||
App help|https://someurl.xyz/help|inappbrowser
|
||||
My grades|https://someurl.xyz/local/mygrades/index.php|embedded|en
|
||||
Mis calificaciones|https://someurl.xyz/local/mygrades/index.php|embedded|es
|
||||
You will only see this in English|https://someurl.xyz/english|browser|en_only
|
||||
</pre>';
|
||||
$string['darkmode'] = 'Dark mode';
|
||||
$string['disabledfeatures'] = 'Disabled features';
|
||||
$string['disabledfeatures_desc'] = 'Select here the features you want to disable in the Mobile app for your site. Please note that some features listed here could be already disabled via other site settings. You will have to log out and log in again in the app to see the changes.';
|
||||
$string['displayerrorswarning'] = 'Display debug messages (debugdisplay) is enabled. It should be disabled.';
|
||||
@@ -84,6 +88,8 @@ $string['mobilecssurl'] = 'CSS';
|
||||
$string['mobilefeatures'] = 'Mobile features';
|
||||
$string['mobilenotificationsdisabledwarning'] = 'Mobile notifications are not enabled. They should be enabled in Notification settings.';
|
||||
$string['mobilesettings'] = 'Mobile settings';
|
||||
$string['moodleappsportalfeatureswarning'] = 'Please note that some features may be restricted depending on your Moodle app subscription. For details, visit the <a href="{$a}" target="_blank">Moodle Apps Portal</a>.';
|
||||
$string['oauth2identityproviders'] = 'OAuth 2 identity providers';
|
||||
$string['offlineuse'] = 'Offline use';
|
||||
$string['pluginname'] = 'Moodle app tools';
|
||||
$string['pluginnotenabledorconfigured'] = 'Plugin not enabled or configured.';
|
||||
|
||||
@@ -30,10 +30,16 @@ require_once($CFG->libdir . '/externallib.php');
|
||||
|
||||
$serviceshortname = required_param('service', PARAM_ALPHANUMEXT);
|
||||
$passport = required_param('passport', PARAM_RAW); // Passport send from the app to validate the response URL.
|
||||
$urlscheme = optional_param('urlscheme', 'moodlemobile', PARAM_ALPHANUM); // The URL scheme the app supports.
|
||||
$urlscheme = optional_param('urlscheme', 'moodlemobile', PARAM_NOTAGS); // The URL scheme the app supports.
|
||||
$confirmed = optional_param('confirmed', false, PARAM_BOOL); // If we are being redirected after user confirmation.
|
||||
$oauthsso = optional_param('oauthsso', 0, PARAM_INT); // Id of the OpenID issuer (for OAuth direct SSO).
|
||||
|
||||
// Validate that the urlscheme is valid.
|
||||
if (!preg_match('/^[a-zA-Z][a-zA-Z0-9-\+\.]*$/', $urlscheme)) {
|
||||
throw new moodle_exception('Invalid parameter: the value of urlscheme isn\'t valid. ' .
|
||||
'It should start with a letter and can only contain letters, numbers and the characters "." "+" "-".');
|
||||
}
|
||||
|
||||
// Check web services enabled.
|
||||
if (!$CFG->enablewebservices) {
|
||||
throw new moodle_exception('enablewsdescription', 'webservice');
|
||||
|
||||
@@ -50,6 +50,12 @@ if ($hassiteconfig) {
|
||||
|
||||
// Show only mobile settings if the mobile service is enabled.
|
||||
if (!empty($CFG->enablemobilewebservice)) {
|
||||
// General notification about limited features due to app restrictions.
|
||||
$notify = new \core\output\notification(
|
||||
get_string('moodleappsportalfeatureswarning', 'tool_mobile', tool_mobile\api::MOODLE_APPS_PORTAL_URL),
|
||||
\core\output\notification::NOTIFY_WARNING);
|
||||
$featuresnotice = $OUTPUT->render($notify);
|
||||
|
||||
// Type of login.
|
||||
$temp = new admin_settingpage('mobileauthentication', new lang_string('mobileauthentication', 'tool_mobile'));
|
||||
$options = array(
|
||||
@@ -63,13 +69,15 @@ if ($hassiteconfig) {
|
||||
|
||||
$temp->add(new admin_setting_configtext('tool_mobile/forcedurlscheme',
|
||||
new lang_string('forcedurlscheme_key', 'tool_mobile'),
|
||||
new lang_string('forcedurlscheme', 'tool_mobile'), '', PARAM_ALPHANUM));
|
||||
new lang_string('forcedurlscheme', 'tool_mobile'), '', PARAM_NOTAGS));
|
||||
|
||||
$ADMIN->add('mobileapp', $temp);
|
||||
|
||||
// Appearance related settings.
|
||||
$temp = new admin_settingpage('mobileappearance', new lang_string('mobileappearance', 'tool_mobile'));
|
||||
|
||||
$temp->add(new admin_setting_heading('tool_mobile/moodleappsportalfeaturesappearance', '', $featuresnotice));
|
||||
|
||||
$temp->add(new admin_setting_configtext('mobilecssurl', new lang_string('mobilecssurl', 'tool_mobile'),
|
||||
new lang_string('configmobilecssurl', 'tool_mobile'), '', PARAM_URL));
|
||||
|
||||
@@ -102,6 +110,8 @@ if ($hassiteconfig) {
|
||||
// Features related settings.
|
||||
$temp = new admin_settingpage('mobilefeatures', new lang_string('mobilefeatures', 'tool_mobile'));
|
||||
|
||||
$temp->add(new admin_setting_heading('tool_mobile/moodleappsportalfeatures', '', $featuresnotice));
|
||||
|
||||
$temp->add(new admin_setting_heading('tool_mobile/logout',
|
||||
new lang_string('logout'), ''));
|
||||
|
||||
|
||||
@@ -131,11 +131,12 @@ if ($execute) {
|
||||
tool_phpunit_problem('Can not create configuration file');
|
||||
}
|
||||
}
|
||||
$configdir = escapeshellarg($configdir);
|
||||
// no cleanup of path - this is tricky because we can not use escapeshellarg and friends for escaping,
|
||||
// this is from admin user so PARAM_PATH must be enough
|
||||
chdir($CFG->dirroot);
|
||||
passthru("php $CFG->admin/tool/phpunit/cli/util.php --run -c $configdir $testclass $testpath", $code);
|
||||
$configdir = escapeshellarg($configdir);
|
||||
$cleanclass = escapeshellarg($testclass);
|
||||
$cleanpath = escapeshellarg($testpath);
|
||||
|
||||
passthru("php $CFG->admin/tool/phpunit/cli/util.php --run -c $configdir $cleanclass $cleanpath", $code);
|
||||
chdir($oldcwd);
|
||||
|
||||
echo '</pre>';
|
||||
@@ -146,7 +147,7 @@ if ($execute) {
|
||||
}
|
||||
|
||||
echo $OUTPUT->box_start('generalbox boxwidthwide boxaligncenter');
|
||||
echo '<form method="get" action="webrunner.php">';
|
||||
echo '<form method="post" action="webrunner.php">';
|
||||
echo '<fieldset class="invisiblefieldset">';
|
||||
echo '<label for="testpath">Test one file</label> ';
|
||||
echo '<input type="text" id="testpath" name="testpath" value="'.s($testpath).'" size="50" /> (all test cases from webrunner.xml if empty)';
|
||||
|
||||
@@ -39,15 +39,29 @@ if ($userid != $USER->id) {
|
||||
if (!has_capability('tool/policy:acceptbehalf', $context)) {
|
||||
require_capability('tool/policy:viewacceptances', $context);
|
||||
}
|
||||
|
||||
$user = core_user::get_user($userid);
|
||||
$PAGE->navigation->extend_for_user($user);
|
||||
}
|
||||
|
||||
$title = get_string('policiesagreements', 'tool_policy');
|
||||
|
||||
$PAGE->set_context($context);
|
||||
$PAGE->set_pagelayout('standard');
|
||||
$PAGE->set_url(new moodle_url('/admin/tool/policy/user.php', ['userid' => $userid]));
|
||||
$PAGE->set_title($title);
|
||||
|
||||
if ($userid == $USER->id &&
|
||||
($profilenode = $PAGE->settingsnav->find('myprofile', null))) {
|
||||
|
||||
$profilenode->make_active();
|
||||
}
|
||||
|
||||
$PAGE->navbar->add($title);
|
||||
|
||||
$output = $PAGE->get_renderer('tool_policy');
|
||||
echo $output->header();
|
||||
echo $output->heading(get_string('policiesagreements', 'tool_policy'));
|
||||
echo $output->heading($title);
|
||||
$acceptances = new \tool_policy\output\acceptances($userid, $returnurl);
|
||||
echo $output->render($acceptances);
|
||||
$PAGE->requires->js_call_amd('tool_policy/acceptmodal', 'getInstance', [context_system::instance()->id]);
|
||||
|
||||
@@ -127,3 +127,36 @@ Feature: Basic recycle bin functionality
|
||||
And I press "Yes"
|
||||
And I should see "Recycle bin has been emptied"
|
||||
And I should see "There are no items in the recycle bin."
|
||||
|
||||
@javascript
|
||||
Scenario: Show recycle bin on category action menu
|
||||
Given I log in as "admin"
|
||||
And I navigate to "Courses > Manage courses and categories" in site administration
|
||||
And I click on "Actions menu" "link"
|
||||
And I click on "Recycle bin" "link"
|
||||
Then I should see "There are no items in the recycle bin."
|
||||
|
||||
@javascript
|
||||
Scenario: Not show recycle bin empty on category action menu whit autohide enable
|
||||
Given I log in as "admin"
|
||||
And the following config values are set as admin:
|
||||
| categorybinenable | 0 | tool_recyclebin |
|
||||
And I navigate to "Courses > Manage courses and categories" in site administration
|
||||
And I click on "Actions menu" "link"
|
||||
Then I should not see "Recycle bin"
|
||||
|
||||
@javascript
|
||||
Scenario: Show recycle bin not empty on category action menu whit autohide enable
|
||||
Given I log in as "admin"
|
||||
And the following config values are set as admin:
|
||||
| autohide | 1 | tool_recyclebin |
|
||||
And I navigate to "Courses > Manage courses and categories" in site administration
|
||||
And I click on "Actions menu" "link"
|
||||
Then I should not see "Recycle bin"
|
||||
And I click on "delete" action for "Course 2" in management course listing
|
||||
And I press "Delete"
|
||||
And I should see "Deleting C2"
|
||||
And I should see "C2 has been completely deleted"
|
||||
And I press "Continue"
|
||||
When I click on "Actions menu" "link"
|
||||
Then I should see "Recycle bin"
|
||||
|
||||
@@ -100,9 +100,9 @@ if (!$problems) {
|
||||
$count = $problem->racount;
|
||||
$edit = array();
|
||||
$aurl = new moodle_url('/admin/roles/define.php', array('roleid'=>$problem->roleid, 'action'=>'edit'));
|
||||
$edit[] = html_writer::link($aurl, $OUTPUT->pix_icon('t/edit', 'core', get_string('edit')));
|
||||
$edit[] = html_writer::link($aurl, $OUTPUT->pix_icon('t/edit', get_string('edit')));
|
||||
$aurl = new moodle_url($PAGE->url, array('roleid'=>$problem->roleid, 'contextlevel'=>$problem->contextlevel, 'action'=>'delete'));
|
||||
$edit[] = html_writer::link($aurl, $OUTPUT->pix_icon('t/delete', 'core', get_string('delete')));
|
||||
$edit[] = html_writer::link($aurl, $OUTPUT->pix_icon('t/delete', get_string('delete')));
|
||||
$data[] = array($levelname, $rolename, $count, implode(' ', $edit));
|
||||
}
|
||||
$table = new html_table();
|
||||
|
||||
@@ -412,6 +412,12 @@ class tool_uploadcourse_course {
|
||||
$this->error('invalidshortname', new lang_string('invalidshortname', 'tool_uploadcourse'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure we don't overflow the maximum length of the shortname field.
|
||||
if (core_text::strlen($this->shortname) > 255) {
|
||||
$this->error('invalidshortnametoolong', new lang_string('invalidshortnametoolong', 'tool_uploadcourse', 255));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$exists = $this->exists();
|
||||
@@ -479,6 +485,12 @@ class tool_uploadcourse_course {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure we don't overflow the maximum length of the fullname field.
|
||||
if (!empty($coursedata['fullname']) && core_text::strlen($coursedata['fullname']) > 254) {
|
||||
$this->error('invalidfullnametoolong', new lang_string('invalidfullnametoolong', 'tool_uploadcourse', 254));
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the course does not exist, or will be forced created.
|
||||
if (!$exists || $mode === tool_uploadcourse_processor::MODE_CREATE_ALL) {
|
||||
|
||||
|
||||
@@ -93,6 +93,8 @@ $string['invalideupdatemode'] = 'Invalid update mode selected';
|
||||
$string['invalidvisibilitymode'] = 'Invalid visible mode';
|
||||
$string['invalidroles'] = 'Invalid role names: {$a}';
|
||||
$string['invalidshortname'] = 'Invalid shortname';
|
||||
$string['invalidfullnametoolong'] = 'The fullname field is limited to {$a} characters';
|
||||
$string['invalidshortnametoolong'] = 'The shortname field is limited to {$a} characters';
|
||||
$string['missingmandatoryfields'] = 'Missing value for mandatory fields: {$a}';
|
||||
$string['missingshortnamenotemplate'] = 'Missing shortname and shortname template not set';
|
||||
$string['mode'] = 'Upload mode';
|
||||
|
||||
@@ -82,6 +82,37 @@ class tool_uploadcourse_course_testcase extends advanced_testcase {
|
||||
$this->assertArrayHasKey('invalidshortname', $co->get_errors());
|
||||
}
|
||||
|
||||
public function test_invalid_shortname_too_long() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
|
||||
$updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
|
||||
|
||||
$upload = new tool_uploadcourse_course($mode, $updatemode, [
|
||||
'category' => 1,
|
||||
'fullname' => 'New course',
|
||||
'shortname' => str_repeat('X', 2000),
|
||||
]);
|
||||
|
||||
$this->assertFalse($upload->prepare());
|
||||
$this->assertArrayHasKey('invalidshortnametoolong', $upload->get_errors());
|
||||
}
|
||||
|
||||
public function test_invalid_fullname_too_long() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
|
||||
$updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
|
||||
|
||||
$upload = new tool_uploadcourse_course($mode, $updatemode, [
|
||||
'category' => 1,
|
||||
'fullname' => str_repeat('X', 2000),
|
||||
]);
|
||||
|
||||
$this->assertFalse($upload->prepare());
|
||||
$this->assertArrayHasKey('invalidfullnametoolong', $upload->get_errors());
|
||||
}
|
||||
|
||||
public function test_invalid_visibility() {
|
||||
$this->resetAfterTest(true);
|
||||
$mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
|
||||
|
||||
@@ -720,6 +720,13 @@ class manager {
|
||||
* @param int $direction
|
||||
*/
|
||||
protected static function _move_tour(tour $tour, $direction) {
|
||||
// We can't move the first tour higher, nor the last tour any lower.
|
||||
if (($tour->is_first_tour() && $direction == helper::MOVE_UP) ||
|
||||
($tour->is_last_tour() && $direction == helper::MOVE_DOWN)) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$currentsortorder = $tour->get_sortorder();
|
||||
$targetsortorder = $currentsortorder + $direction;
|
||||
|
||||
@@ -848,6 +855,9 @@ class manager {
|
||||
}
|
||||
$existingtourrecords->close();
|
||||
|
||||
// Ensure we correct the sortorder in any existing tours, prior to adding latest shipped tours.
|
||||
helper::reset_tour_sortorder();
|
||||
|
||||
foreach (array_reverse($shippedtours) as $filename => $version) {
|
||||
$filepath = $CFG->dirroot . "/{$CFG->admin}/tool/usertours/tours/" . $filename;
|
||||
$tourjson = file_get_contents($filepath);
|
||||
|
||||
@@ -64,5 +64,12 @@ function xmldb_tool_usertours_upgrade($oldversion) {
|
||||
// Automatically generated Moodle v3.7.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
if ($oldversion < 2019052001) {
|
||||
// Updating shipped tours will fix broken sortorder records in existing tours.
|
||||
manager::update_shipped_tours();
|
||||
|
||||
upgrade_plugin_savepoint(true, 2019052001, 'tool', 'usertours');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -122,6 +122,83 @@ class tool_usertours_manager_testcase extends advanced_testcase {
|
||||
$rcm->invokeArgs($manager, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_move_tour
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function move_tour_provider() {
|
||||
$alltours = [
|
||||
['name' => 'Tour 1'],
|
||||
['name' => 'Tour 2'],
|
||||
['name' => 'Tour 3'],
|
||||
];
|
||||
|
||||
return [
|
||||
'Move up' => [
|
||||
$alltours,
|
||||
'Tour 2',
|
||||
\tool_usertours\helper::MOVE_UP,
|
||||
0,
|
||||
],
|
||||
'Move down' => [
|
||||
$alltours,
|
||||
'Tour 2',
|
||||
\tool_usertours\helper::MOVE_DOWN,
|
||||
2,
|
||||
],
|
||||
'Move up (first)' => [
|
||||
$alltours,
|
||||
'Tour 1',
|
||||
\tool_usertours\helper::MOVE_UP,
|
||||
0,
|
||||
],
|
||||
'Move down (last)' => [
|
||||
$alltours,
|
||||
'Tour 3',
|
||||
\tool_usertours\helper::MOVE_DOWN,
|
||||
2,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test moving tours (changing sortorder)
|
||||
*
|
||||
* @dataProvider move_tour_provider
|
||||
*
|
||||
* @param array $alltours
|
||||
* @param string $movetourname
|
||||
* @param int $direction
|
||||
* @param int $expectedsortorder
|
||||
* @return void
|
||||
*/
|
||||
public function test_move_tour($alltours, $movetourname, $direction, $expectedsortorder) {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Clear out existing tours so ours are the only ones, otherwise we can't predict the sortorder.
|
||||
$DB->delete_records('tool_usertours_tours');
|
||||
|
||||
foreach ($alltours as $tourconfig) {
|
||||
$this->helper_create_tour((object) $tourconfig);
|
||||
}
|
||||
|
||||
// Load our tour to move.
|
||||
$record = $DB->get_record('tool_usertours_tours', ['name' => $movetourname]);
|
||||
$tour = \tool_usertours\tour::load_from_record($record);
|
||||
|
||||
// Call protected method via reflection.
|
||||
$class = new ReflectionClass(\tool_usertours\manager::class);
|
||||
$method = $class->getMethod('_move_tour');
|
||||
$method->setAccessible(true);
|
||||
$method->invokeArgs(null, [$tour, $direction]);
|
||||
|
||||
// Assert expected sortorder.
|
||||
$this->assertEquals($expectedsortorder, $tour->get_sortorder());
|
||||
}
|
||||
|
||||
/**
|
||||
* Data Provider for get_matching_tours tests.
|
||||
*
|
||||
|
||||
@@ -24,6 +24,6 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2019052000; // The current module version (Date: YYYYMMDDXX).
|
||||
$plugin->version = 2019052001; // The current module version (Date: YYYYMMDDXX).
|
||||
$plugin->requires = 2019051100; // Requires this Moodle version.
|
||||
$plugin->component = 'tool_usertours'; // Full name of the plugin (used for diagnostics).
|
||||
|
||||
@@ -68,13 +68,13 @@ class view_xml extends XMLDBAction {
|
||||
|
||||
// Get the file parameter
|
||||
$file = required_param('file', PARAM_PATH);
|
||||
$file = $CFG->dirroot . $file;
|
||||
// File must be under $CFG->wwwroot and
|
||||
// under one db directory (simple protection)
|
||||
if (substr($file, 0, strlen($CFG->dirroot)) == $CFG->dirroot &&
|
||||
substr(dirname($file), -2, 2) == 'db') {
|
||||
|
||||
$fullpath = $CFG->dirroot . $file;
|
||||
// File param must start with / and end with /db/install.xml to be safe.
|
||||
if (substr($file, 0, 1) == '/' &&
|
||||
substr($file, -15, 15) == '/db/install.xml') {
|
||||
// Everything is ok. Load the file to memory
|
||||
$this->output = file_get_contents($file);
|
||||
$this->output = file_get_contents($fullpath);
|
||||
} else {
|
||||
// Switch to HTML and error
|
||||
$this->does_generate = ACTION_GENERATE_HTML;
|
||||
|
||||
@@ -149,7 +149,7 @@ class auth_email_external extends external_api {
|
||||
new external_single_structure(
|
||||
array(
|
||||
'id' => new external_value(PARAM_INT, 'Profile field id', VALUE_OPTIONAL),
|
||||
'shortname' => new external_value(PARAM_ALPHANUM, 'Password policy', VALUE_OPTIONAL),
|
||||
'shortname' => new external_value(PARAM_ALPHANUMEXT, 'Profile field shortname', VALUE_OPTIONAL),
|
||||
'name' => new external_value(PARAM_TEXT, 'Profield field name', VALUE_OPTIONAL),
|
||||
'datatype' => new external_value(PARAM_ALPHANUMEXT, 'Profield field datatype', VALUE_OPTIONAL),
|
||||
'description' => new external_value(PARAM_RAW, 'Profield field description', VALUE_OPTIONAL),
|
||||
|
||||
@@ -263,7 +263,8 @@ class auth_plugin_shibboleth extends auth_plugin_base {
|
||||
global $OUTPUT;
|
||||
|
||||
if (!isset($this->config->user_attribute) || empty($this->config->user_attribute)) {
|
||||
echo $OUTPUT->notification(get_string("shib_not_set_up_error", "auth_shibboleth"), 'notifyproblem');
|
||||
echo $OUTPUT->notification(get_string("shib_not_set_up_error", "auth_shibboleth",
|
||||
(new moodle_url('/auth/shibboleth/README.txt'))->out()), 'notifyproblem');
|
||||
return;
|
||||
}
|
||||
if ($this->config->convert_data and $this->config->convert_data != '' and !is_readable($this->config->convert_data)) {
|
||||
|
||||
@@ -32,8 +32,9 @@
|
||||
$shibbolethauth = get_auth_plugin('shibboleth');
|
||||
|
||||
// Check whether Shibboleth is configured properly
|
||||
$readmeurl = (new moodle_url('/auth/shibboleth/README.txt'))->out();
|
||||
if (empty($pluginconfig->user_attribute)) {
|
||||
print_error('shib_not_set_up_error', 'auth_shibboleth');
|
||||
print_error('shib_not_set_up_error', 'auth_shibboleth', '', $readmeurl);
|
||||
}
|
||||
|
||||
/// If we can find the Shibboleth attribute, save it in session and return to main login page
|
||||
@@ -91,7 +92,7 @@
|
||||
elseif (!empty($_SERVER['HTTP_SHIB_APPLICATION_ID']) || !empty($_SERVER['Shib-Application-ID'])) {
|
||||
print_error('shib_no_attributes_error', 'auth_shibboleth' , '', '\''.$pluginconfig->user_attribute.'\', \''.$pluginconfig->field_map_firstname.'\', \''.$pluginconfig->field_map_lastname.'\' and \''.$pluginconfig->field_map_email.'\'');
|
||||
} else {
|
||||
print_error('shib_not_set_up_error', 'auth_shibboleth');
|
||||
print_error('shib_not_set_up_error', 'auth_shibboleth', '', $readmeurl);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ $string['auth_shib_auth_method_description'] = 'Provide a name for the Shibbolet
|
||||
$string['auth_shib_auth_logo'] = 'Authentication method logo';
|
||||
$string['auth_shib_auth_logo_description'] = 'Provide a logo for the Shibboleth authentication method that is familiar to your users. This could be the logo of your Shibboleth federation, e.g. <tt>SWITCHaai Login</tt> or <tt>InCommon Login</tt> or similar.';
|
||||
$string['auth_shib_contact_administrator'] = 'In case you are not associated with the given organizations and you need access to a course on this server, please contact the <a href="mailto:{$a}">Moodle Administrator</a>.';
|
||||
$string['auth_shibbolethdescription'] = 'Using this method users are created and authenticated using Shibboleth. For set-up details, see the <a href="../auth/shibboleth/README.txt">Shibboleth README</a>.';
|
||||
$string['auth_shibbolethdescription'] = 'Using this method users are created and authenticated using Shibboleth. For set-up details, see the <a href="{$a}">Shibboleth README</a>.';
|
||||
$string['auth_shibboleth_errormsg'] = 'Please select the organization you are member of!';
|
||||
$string['auth_shibboleth_login'] = 'Shibboleth login';
|
||||
$string['auth_shibboleth_login_long'] = 'Login to Moodle via Shibboleth';
|
||||
@@ -36,7 +36,7 @@ $string['auth_shibboleth_manual_login'] = 'Manual login';
|
||||
$string['auth_shibboleth_select_member'] = 'I\'m a member of ...';
|
||||
$string['auth_shibboleth_select_organization'] = 'For authentication via Shibboleth, please select your organisation from the drop-down menu:';
|
||||
$string['auth_shib_convert_data'] = 'Data modification API';
|
||||
$string['auth_shib_convert_data_description'] = 'You can use this API to further modify the data provided by Shibboleth. Read the <a href="../auth/shibboleth/README.txt">README</a> for further instructions.';
|
||||
$string['auth_shib_convert_data_description'] = 'You can use this API to further modify the data provided by Shibboleth. Read the <a href="{$a}">README</a> for further instructions.';
|
||||
$string['auth_shib_convert_data_warning'] = 'The file does not exist or is not readable by the webserver process!';
|
||||
$string['auth_shib_changepasswordurl'] = 'Password-change URL';
|
||||
$string['auth_shib_idp_list'] = 'Identity providers';
|
||||
@@ -57,6 +57,6 @@ $string['auth_shib_username_description'] = 'Name of the webserver Shibboleth en
|
||||
$string['shib_invalid_account_error'] = 'You seem to be Shibboleth authenticated but Moodle has no valid account for your username. Your account may not exist or it may have been suspended.';
|
||||
$string['shib_no_attributes_error'] = 'You seem to be Shibboleth authenticated but Moodle didn\'t receive any user attributes. Please check that your Identity Provider releases the necessary attributes ({$a}) to the Service Provider Moodle is running on or inform the webmaster of this server.';
|
||||
$string['shib_not_all_attributes_error'] = 'Moodle needs certain Shibboleth attributes which are not present in your case. The attributes are: {$a}<br />Please contact the webmaster of this server or your Identity Provider.';
|
||||
$string['shib_not_set_up_error'] = 'Shibboleth authentication doesn\'t seem to be set up correctly because no Shibboleth environment variables are present for this page. Please consult the <a href="README.txt">README</a> for further instructions on how to set up Shibboleth authentication or contact the webmaster of this Moodle installation.';
|
||||
$string['shib_not_set_up_error'] = 'Shibboleth authentication doesn\'t seem to be set up correctly because no Shibboleth environment variables are present for this page. Please consult the <a href="{$a}">README</a> for further instructions on how to set up Shibboleth authentication or contact the webmaster of this Moodle installation.';
|
||||
$string['pluginname'] = 'Shibboleth';
|
||||
$string['privacy:metadata'] = 'The Shibboleth authentication plugin does not store any personal data.';
|
||||
|
||||
@@ -30,8 +30,9 @@ if ($ADMIN->fulltree) {
|
||||
require_once($CFG->dirroot.'/auth/shibboleth/classes/admin_setting_special_idp_configtextarea.php');
|
||||
|
||||
// Introductory explanation.
|
||||
$readmeurl = (new moodle_url('/auth/shibboleth/README.txt'))->out();
|
||||
$settings->add(new admin_setting_heading('auth_shibboleth/pluginname', '',
|
||||
new lang_string('auth_shibbolethdescription', 'auth_shibboleth')));
|
||||
new lang_string('auth_shibbolethdescription', 'auth_shibboleth', $readmeurl)));
|
||||
|
||||
// Username.
|
||||
$settings->add(new admin_setting_configtext('auth_shibboleth/user_attribute', get_string('username'),
|
||||
@@ -40,7 +41,7 @@ if ($ADMIN->fulltree) {
|
||||
// COnvert Data configuration file.
|
||||
$settings->add(new admin_setting_configfile('auth_shibboleth/convert_data',
|
||||
get_string('auth_shib_convert_data', 'auth_shibboleth'),
|
||||
get_string('auth_shib_convert_data_description', 'auth_shibboleth'), ''));
|
||||
get_string('auth_shib_convert_data_description', 'auth_shibboleth', $readmeurl), ''));
|
||||
|
||||
// WAYF.
|
||||
$settings->add(new auth_shibboleth_admin_setting_special_wayf_select());
|
||||
|
||||
@@ -91,7 +91,6 @@ class core_backup_external extends external_api {
|
||||
require_capability('moodle/backup:backupactivity', $context);
|
||||
} else {
|
||||
require_capability('moodle/backup:backupcourse', $context);
|
||||
$instanceid = $course->id;
|
||||
}
|
||||
|
||||
$results = array();
|
||||
|
||||
@@ -955,6 +955,52 @@ class core_backup_moodle2_testcase extends advanced_testcase {
|
||||
$this->assertEquals('', $requests[2]->searcharea);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test restoring courses based on the backup plan. Primarily used with
|
||||
* the import functionality
|
||||
*/
|
||||
public function test_restore_course_using_plan_defaults() {
|
||||
global $DB, $CFG, $USER;
|
||||
|
||||
$this->resetAfterTest(true);
|
||||
$this->setAdminUser();
|
||||
$CFG->enableglobalsearch = true;
|
||||
|
||||
// Set admin config setting so that activities are not restored by default.
|
||||
set_config('restore_general_activities', 0, 'restore');
|
||||
|
||||
// Create a course.
|
||||
$generator = $this->getDataGenerator();
|
||||
$course = $generator->create_course();
|
||||
$course2 = $generator->create_course();
|
||||
$course3 = $generator->create_course();
|
||||
|
||||
// Add a forum.
|
||||
$forum = $generator->create_module('forum', ['course' => $course->id]);
|
||||
|
||||
// Backup course...
|
||||
$CFG->backup_file_logger_level = backup::LOG_NONE;
|
||||
$bc = new backup_controller(backup::TYPE_1COURSE, $course->id,
|
||||
backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
|
||||
$USER->id);
|
||||
$backupid = $bc->get_backupid();
|
||||
$bc->execute_plan();
|
||||
$bc->destroy();
|
||||
|
||||
// Restore it on top of course2 (should duplicate the forum).
|
||||
$rc = new restore_controller($backupid, $course2->id,
|
||||
backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id,
|
||||
backup::TARGET_EXISTING_ADDING, null);
|
||||
$this->assertTrue($rc->execute_precheck());
|
||||
$rc->execute_plan();
|
||||
$rc->destroy();
|
||||
|
||||
// Get the forums now on the old course.
|
||||
$modinfo = get_fast_modinfo($course2->id);
|
||||
$forums = $modinfo->get_instances_of('forum');
|
||||
$this->assertCount(0, $forums);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Question category hierarchical structure was changed in Moodle 3.5.
|
||||
* From 3.5, all question categories in each context are a child of a single top level question category for that context.
|
||||
|
||||
+2
-2
@@ -55,9 +55,11 @@ require_capability('moodle/restore:restorecourse', $context);
|
||||
if (is_null($course)) {
|
||||
$coursefullname = $SITE->fullname;
|
||||
$courseshortname = $SITE->shortname;
|
||||
$courseurl = new moodle_url('/');
|
||||
} else {
|
||||
$coursefullname = $course->fullname;
|
||||
$courseshortname = $course->shortname;
|
||||
$courseurl = course_get_url($course->id);
|
||||
}
|
||||
|
||||
// Show page header.
|
||||
@@ -173,7 +175,6 @@ if ($restore->get_stage() != restore_ui::STAGE_PROCESS) {
|
||||
\core\task\manager::queue_adhoc_task($asynctask);
|
||||
|
||||
// Add ajax progress bar and initiate ajax via a template.
|
||||
$courseurl = new moodle_url('/course/view.php', array('id' => $course->id));
|
||||
$restoreurl = new moodle_url('/backup/restorefile.php', array('contextid' => $contextid));
|
||||
$progresssetup = array(
|
||||
'backupid' => $restoreid,
|
||||
@@ -182,7 +183,6 @@ if ($restore->get_stage() != restore_ui::STAGE_PROCESS) {
|
||||
'restoreurl' => $restoreurl->out()
|
||||
);
|
||||
echo $renderer->render_from_template('core/async_backup_status', $progresssetup);
|
||||
|
||||
}
|
||||
|
||||
$restore->destroy();
|
||||
|
||||
@@ -282,7 +282,14 @@ abstract class restore_controller_dbops extends restore_dbops {
|
||||
if ($plan->setting_exists($settingname)) {
|
||||
$setting = $plan->get_setting($settingname);
|
||||
$value = self::get_setting_default($config, $setting);
|
||||
$locked = (get_config('restore', $config . '_locked') == true);
|
||||
$locked = (get_config('restore',$config . '_locked') == true);
|
||||
|
||||
// Use the original value when this is an import and the setting is unlocked.
|
||||
if ($controller->get_mode() == backup::MODE_IMPORT && $controller->get_interactive()) {
|
||||
if (!$uselocks || !$locked) {
|
||||
$value = $setting->get_value();
|
||||
}
|
||||
}
|
||||
|
||||
// We can only update the setting if it isn't already locked by config or permission.
|
||||
if ($setting->get_status() != base_setting::LOCKED_BY_CONFIG
|
||||
|
||||
@@ -1371,7 +1371,9 @@ abstract class restore_dbops {
|
||||
// Note: for DB deleted users md5(username) is stored *sometimes* in the email field,
|
||||
// hence we are looking there for usernames if not empty. See delete_user()
|
||||
// If match by id and mnethost and user is deleted in DB and
|
||||
// match by username LIKE 'backup_email.%' or by non empty email = md5(username) => ok, return target user
|
||||
// match by username LIKE 'substring(backup_email).%' where the substr length matches the retained data in the
|
||||
// username field (100 - (timestamp + 1) characters), or by non empty email = md5(username) => ok, return target user.
|
||||
$usernamelookup = core_text::substr($user->email, 0, 89) . '.%';
|
||||
if ($rec = $DB->get_record_sql("SELECT *
|
||||
FROM {user} u
|
||||
WHERE id = ?
|
||||
@@ -1384,13 +1386,14 @@ abstract class restore_dbops {
|
||||
AND email = ?
|
||||
)
|
||||
)",
|
||||
array($user->id, $user->mnethostid, $user->email.'.%', md5($user->username)))) {
|
||||
array($user->id, $user->mnethostid, $usernamelookup, md5($user->username)))) {
|
||||
return $rec; // Matching user, deleted in DB found, return it
|
||||
}
|
||||
|
||||
// 1D - Handle users deleted in backup file and "alive" in DB
|
||||
// If match by id and mnethost and user is deleted in backup file
|
||||
// and match by email = email_without_time(backup_email) => ok, return target user
|
||||
// and match by substring(email) = email_without_time(backup_email) where the substr length matches the retained data
|
||||
// in the username field (100 - (timestamp + 1) characters) => ok, return target user.
|
||||
if ($user->deleted) {
|
||||
// Note: for DB deleted users email is stored in username field, hence we
|
||||
// are looking there for emails. See delete_user()
|
||||
@@ -1400,7 +1403,7 @@ abstract class restore_dbops {
|
||||
FROM {user} u
|
||||
WHERE id = ?
|
||||
AND mnethostid = ?
|
||||
AND UPPER(email) = UPPER(?)",
|
||||
AND " . $DB->sql_substr('UPPER(email)', 1, 89) . " = UPPER(?)",
|
||||
array($user->id, $user->mnethostid, $trimemail))) {
|
||||
return $rec; // Matching user, deleted in backup file found, return it
|
||||
}
|
||||
@@ -1447,7 +1450,8 @@ abstract class restore_dbops {
|
||||
// Note: for DB deleted users md5(username) is stored *sometimes* in the email field,
|
||||
// hence we are looking there for usernames if not empty. See delete_user()
|
||||
// 2B1 - If match by mnethost and user is deleted in DB and not empty email = md5(username) and
|
||||
// (by username LIKE 'backup_email.%' or non-zero firstaccess) => ok, return target user
|
||||
// (by username LIKE 'substring(backup_email).%' or non-zero firstaccess) => ok, return target user.
|
||||
$usernamelookup = core_text::substr($user->email, 0, 89) . '.%';
|
||||
if ($rec = $DB->get_record_sql("SELECT *
|
||||
FROM {user} u
|
||||
WHERE mnethostid = ?
|
||||
@@ -1461,14 +1465,15 @@ abstract class restore_dbops {
|
||||
AND firstaccess = ?
|
||||
)
|
||||
)",
|
||||
array($user->mnethostid, md5($user->username), $user->email.'.%', $user->firstaccess))) {
|
||||
array($user->mnethostid, md5($user->username), $usernamelookup, $user->firstaccess))) {
|
||||
return $rec; // Matching user found, return it
|
||||
}
|
||||
|
||||
// 2B2 - If match by mnethost and user is deleted in DB and
|
||||
// username LIKE 'backup_email.%' and non-zero firstaccess) => ok, return target user
|
||||
// username LIKE 'substring(backup_email).%' and non-zero firstaccess) => ok, return target user
|
||||
// (this covers situations where md5(username) wasn't being stored so we require both
|
||||
// the email & non-zero firstaccess to match)
|
||||
$usernamelookup = core_text::substr($user->email, 0, 89) . '.%';
|
||||
if ($rec = $DB->get_record_sql("SELECT *
|
||||
FROM {user} u
|
||||
WHERE mnethostid = ?
|
||||
@@ -1476,13 +1481,13 @@ abstract class restore_dbops {
|
||||
AND UPPER(username) LIKE UPPER(?)
|
||||
AND firstaccess != 0
|
||||
AND firstaccess = ?",
|
||||
array($user->mnethostid, $user->email.'.%', $user->firstaccess))) {
|
||||
array($user->mnethostid, $usernamelookup, $user->firstaccess))) {
|
||||
return $rec; // Matching user found, return it
|
||||
}
|
||||
|
||||
// 2C - Handle users deleted in backup file and "alive" in DB
|
||||
// If match mnethost and user is deleted in backup file
|
||||
// and match by email = email_without_time(backup_email) and non-zero firstaccess=> ok, return target user
|
||||
// and match by substring(email) = email_without_time(backup_email) and non-zero firstaccess=> ok, return target user.
|
||||
if ($user->deleted) {
|
||||
// Note: for DB deleted users email is stored in username field, hence we
|
||||
// are looking there for emails. See delete_user()
|
||||
@@ -1491,7 +1496,7 @@ abstract class restore_dbops {
|
||||
if ($rec = $DB->get_record_sql("SELECT *
|
||||
FROM {user} u
|
||||
WHERE mnethostid = ?
|
||||
AND UPPER(email) = UPPER(?)
|
||||
AND " . $DB->sql_substr('UPPER(email)', 1, 89) . " = UPPER(?)
|
||||
AND firstaccess != 0
|
||||
AND firstaccess = ?",
|
||||
array($user->mnethostid, $trimemail, $user->firstaccess))) {
|
||||
|
||||
@@ -119,4 +119,246 @@ class restore_dbops_testcase extends advanced_testcase {
|
||||
$this->assertSame('Table "backup_ids_temp" does not exist', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for {@link test_precheck_user()}
|
||||
*/
|
||||
public function precheck_user_provider() {
|
||||
|
||||
$emailmultiplier = [
|
||||
'shortmail' => 'normalusername@example.com',
|
||||
'longmail' => str_repeat('a', 100) // It's not validated, hence any string is ok.
|
||||
];
|
||||
|
||||
$providercases = [];
|
||||
|
||||
foreach ($emailmultiplier as $emailk => $email) {
|
||||
// Get the related cases.
|
||||
$cases = $this->precheck_user_cases($email);
|
||||
// Rename them (keys).
|
||||
foreach ($cases as $key => $case) {
|
||||
$providercases[$key . ' - ' . $emailk] = $case;
|
||||
}
|
||||
}
|
||||
|
||||
return $providercases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the cases implemented in {@link restore_dbops::precheck_users()}
|
||||
*
|
||||
* @param string $email
|
||||
*/
|
||||
private function precheck_user_cases($email) {
|
||||
global $CFG;
|
||||
|
||||
$baseuserarr = [
|
||||
'username' => 'normalusername',
|
||||
'email' => $email,
|
||||
'mnethostid' => $CFG->mnet_localhost_id,
|
||||
'firstaccess' => 123456789,
|
||||
'deleted' => 0,
|
||||
'forceemailcleanup' => false, // Hack to force the DB record to have empty mail.
|
||||
'forceduplicateadminallowed' => false]; // Hack to enable import_general_duplicate_admin_allowed.
|
||||
|
||||
return [
|
||||
// Cases with samesite = true.
|
||||
'samesite match existing (1A)' => [
|
||||
'dbuser' => $baseuserarr,
|
||||
'backupuser' => $baseuserarr,
|
||||
'samesite' => true,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'samesite match existing anon (1B)' => [
|
||||
'dbuser' => array_merge($baseuserarr, [
|
||||
'username' => 'anon01']),
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'id' => -1, 'username' => 'anon01', 'firstname' => 'anonfirstname01',
|
||||
'lastname' => 'anonlastname01', 'email' => 'anon01@doesntexist.invalid']),
|
||||
'samesite' => true,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'samesite match existing deleted in db, alive in backup, by db username (1C)' => [
|
||||
'dbuser' => array_merge($baseuserarr, [
|
||||
'deleted' => 1]),
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'username' => 'this_wont_match']),
|
||||
'samesite' => true,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'samesite match existing deleted in db, alive in backup, by db email (1C)' => [
|
||||
'dbuser' => array_merge($baseuserarr, [
|
||||
'deleted' => 1]),
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'email' => 'this_wont_match']),
|
||||
'samesite' => true,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'samesite match existing alive in db, deleted in backup (1D)' => [
|
||||
'dbuser' => $baseuserarr,
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'deleted' => 1]),
|
||||
'samesite' => true,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'samesite conflict (1E)' => [
|
||||
'dbuser' => $baseuserarr,
|
||||
'backupuser' => array_merge($baseuserarr, ['id' => -1]),
|
||||
'samesite' => true,
|
||||
'outcome' => false
|
||||
],
|
||||
'samesite create user (1F)' => [
|
||||
'dbuser' => $baseuserarr,
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'username' => 'newusername']),
|
||||
'samesite' => false,
|
||||
'outcome' => true
|
||||
],
|
||||
|
||||
// Cases with samesite = false.
|
||||
'no samesite match existing, by db email (2A1)' => [
|
||||
'dbuser' => $baseuserarr,
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'firstaccess' => 0]),
|
||||
'samesite' => false,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'no samesite match existing, by db firstaccess (2A1)' => [
|
||||
'dbuser' => $baseuserarr,
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'email' => 'this_wont_match@example.con']),
|
||||
'samesite' => false,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'no samesite match existing anon (2A1 too)' => [
|
||||
'dbuser' => array_merge($baseuserarr, [
|
||||
'username' => 'anon01']),
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'id' => -1, 'username' => 'anon01', 'firstname' => 'anonfirstname01',
|
||||
'lastname' => 'anonlastname01', 'email' => 'anon01@doesntexist.invalid']),
|
||||
'samesite' => false,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'no samesite match dupe admin (2A2)' => [
|
||||
'dbuser' => array_merge($baseuserarr, [
|
||||
'username' => 'admin_old_site_id',
|
||||
'forceduplicateadminallowed' => true]),
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'username' => 'admin']),
|
||||
'samesite' => false,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'no samesite match existing deleted in db, alive in backup, by db username (2B1)' => [
|
||||
'dbuser' => array_merge($baseuserarr, [
|
||||
'deleted' => 1]),
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'firstaccess' => 0]),
|
||||
'samesite' => false,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'no samesite match existing deleted in db, alive in backup, by db firstaccess (2B1)' => [
|
||||
'dbuser' => array_merge($baseuserarr, [
|
||||
'deleted' => 1]),
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'mail' => 'this_wont_match']),
|
||||
'samesite' => false,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'no samesite match existing deleted in db, alive in backup (2B2)' => [
|
||||
'dbuser' => array_merge($baseuserarr, [
|
||||
'deleted' => 1,
|
||||
'forceemailcleanup' => true]),
|
||||
'backupuser' => $baseuserarr,
|
||||
'samesite' => false,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'no samesite match existing alive in db, deleted in backup (2C)' => [
|
||||
'dbuser' => $baseuserarr,
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'deleted' => 1]),
|
||||
'samesite' => false,
|
||||
'outcome' => 'match'
|
||||
],
|
||||
'no samesite conflict (2D)' => [
|
||||
'dbuser' => $baseuserarr,
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'email' => 'anotheruser@example.com', 'firstaccess' => 0]),
|
||||
'samesite' => false,
|
||||
'outcome' => false
|
||||
],
|
||||
'no samesite create user (2E)' => [
|
||||
'dbuser' => $baseuserarr,
|
||||
'backupuser' => array_merge($baseuserarr, [
|
||||
'username' => 'newusername']),
|
||||
'samesite' => false,
|
||||
'outcome' => true
|
||||
],
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test restore precheck_user method
|
||||
*
|
||||
* @dataProvider precheck_user_provider
|
||||
* @covers restore_dbops::precheck_user()
|
||||
*
|
||||
* @param array $dbuser
|
||||
* @param array $backupuser
|
||||
* @param bool $samesite
|
||||
* @param mixed $outcome
|
||||
**/
|
||||
public function test_precheck_user($dbuser, $backupuser, $samesite, $outcome) {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$dbuser = (object)$dbuser;
|
||||
$backupuser = (object)$backupuser;
|
||||
|
||||
$siteid = null;
|
||||
|
||||
// If the backup user must be deleted, simulate it (by temp inserting to DB, deleting and fetching it back).
|
||||
if ($backupuser->deleted) {
|
||||
$backupuser->id = $DB->insert_record('user', array_merge((array)$backupuser, ['deleted' => 0]));
|
||||
delete_user($backupuser);
|
||||
$backupuser = $DB->get_record('user', ['id' => $backupuser->id]);
|
||||
$DB->delete_records('user', ['id' => $backupuser->id]);
|
||||
unset($backupuser->id);
|
||||
}
|
||||
|
||||
// Create the db user, normally.
|
||||
$dbuser->id = $DB->insert_record('user', array_merge((array)$dbuser, ['deleted' => 0]));
|
||||
$backupuser->id = $backupuser->id ?? $dbuser->id;
|
||||
|
||||
// We may want to enable the import_general_duplicate_admin_allowed setting and look for old admin records.
|
||||
if ($dbuser->forceduplicateadminallowed) {
|
||||
set_config('import_general_duplicate_admin_allowed', true, 'backup');
|
||||
$siteid = 'old_site_id';
|
||||
}
|
||||
|
||||
// If the DB user must be deleted, do it and fetch it back.
|
||||
if ($dbuser->deleted) {
|
||||
delete_user($dbuser);
|
||||
// We may want to clean the mail field (old behavior, not containing the current md5(username).
|
||||
if ($dbuser->forceemailcleanup) {
|
||||
$DB->set_field('user', 'email', '', ['id' => $dbuser->id]);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the dbuser record, because we may have changed it above.
|
||||
$dbuser = $DB->get_record('user', ['id' => $dbuser->id]);
|
||||
|
||||
$method = (new ReflectionClass('restore_dbops'))->getMethod('precheck_user');
|
||||
$method->setAccessible(true);
|
||||
$result = $method->invoke(null, $backupuser, $samesite, $siteid);
|
||||
|
||||
if (is_bool($result)) {
|
||||
$this->assertSame($outcome, $result);
|
||||
} else {
|
||||
$outcome = $dbuser; // Outcome is not bool, matching found, so it must be the dbuser,
|
||||
// Just check ids, it means the expected match has been found in database.
|
||||
$this->assertSame($outcome->id, $result->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,10 +305,17 @@ class async_helper {
|
||||
$tabledata = array();
|
||||
|
||||
// Get relevant backup ids based on context instance id.
|
||||
$select = 'itemid = ? AND execution = ? AND status < ? AND status > ?';
|
||||
$params = array($instanceid, backup::EXECUTION_DELAYED, backup::STATUS_FINISHED_ERR, backup::STATUS_NEED_PRECHECK);
|
||||
$backups = $DB->get_records_select('backup_controllers', $select, $params, 'timecreated DESC', 'id, backupid, timecreated');
|
||||
$select = 'itemid = :itemid AND execution = :execution AND status < :status1 AND status > :status2 ' .
|
||||
'AND operation = :operation';
|
||||
$params = [
|
||||
'itemid' => $instanceid,
|
||||
'execution' => backup::EXECUTION_DELAYED,
|
||||
'status1' => backup::STATUS_FINISHED_ERR,
|
||||
'status2' => backup::STATUS_NEED_PRECHECK,
|
||||
'operation' => 'backup',
|
||||
];
|
||||
|
||||
$backups = $DB->get_records_select('backup_controllers', $select, $params, 'timecreated DESC', 'id, backupid, timecreated');
|
||||
foreach ($backups as $backup) {
|
||||
$bc = \backup_controller::load_controller($backup->backupid); // Get the backup controller.
|
||||
$filename = $bc->get_plan()->get_setting('filename')->get_value();
|
||||
|
||||
@@ -199,7 +199,7 @@ class core_badges_assertion {
|
||||
$class['criteria'] = $this->_url->out(false); // Currently issued badge URL.
|
||||
if ($issued) {
|
||||
if ($this->_obversion == OPEN_BADGES_V2) {
|
||||
$issuerurl = new moodle_url('/badges/badge_json.php', array('id' => $this->get_badge_id(), 'action' => 0));
|
||||
$issuerurl = new moodle_url('/badges/issuer_json.php', array('id' => $this->get_badge_id()));
|
||||
} else {
|
||||
$issuerurl = new moodle_url('/badges/assertion.php', array('b' => $this->_data->uniquehash, 'action' => 0));
|
||||
}
|
||||
@@ -233,7 +233,8 @@ class core_badges_assertion {
|
||||
$issuer['email'] = $CFG->badges_defaultissuercontact;
|
||||
}
|
||||
} else {
|
||||
$issuer = badges_get_default_issuer();
|
||||
$badge = new badge($this->get_badge_id());
|
||||
$issuer = $badge->get_badge_issuer();
|
||||
}
|
||||
}
|
||||
$this->embed_data_badge_version2($issuer, OPEN_BADGES_V2_TYPE_ISSUER);
|
||||
@@ -341,8 +342,7 @@ class core_badges_assertion {
|
||||
'/badges/badge_json.php',
|
||||
array('id' => $this->get_badge_id())
|
||||
);
|
||||
$issuerurl = new moodle_url('/badges/badge_json.php', array('id' => $this->get_badge_id(), 'action' => 0,
|
||||
'obversion' => $this->_obversion));
|
||||
$issuerurl = new moodle_url('/badges/issuer_json.php', ['id' => $this->get_badge_id()]);
|
||||
// For assertion.
|
||||
if ($type == OPEN_BADGES_V2_TYPE_ASSERTION) {
|
||||
$json['@context'] = OPEN_BADGES_V2_CONTEXT;
|
||||
|
||||
@@ -929,12 +929,12 @@ class badge {
|
||||
*/
|
||||
public function get_badge_issuer() {
|
||||
$issuer = array();
|
||||
$issuerurl = new moodle_url('/badges/badge_json.php', array('id' => $this->id, 'action' => 0));
|
||||
$issuer['name'] = $this->issuername;
|
||||
$issuer['url'] = $this->issuerurl;
|
||||
$issuer['email'] = $this->issuercontact;
|
||||
$issuer['@context'] = OPEN_BADGES_V2_CONTEXT;
|
||||
$issuer['id'] = $this->issuerurl;
|
||||
$issueridurl = new moodle_url('/badges/issuer_json.php', array('id' => $this->id));
|
||||
$issuer['id'] = $issueridurl->out(false);
|
||||
$issuer['type'] = OPEN_BADGES_V2_TYPE_ISSUER;
|
||||
return $issuer;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ class badge extends moodleform {
|
||||
$mform = $this->_form;
|
||||
$badge = (isset($this->_customdata['badge'])) ? $this->_customdata['badge'] : false;
|
||||
$action = $this->_customdata['action'];
|
||||
$languages = get_string_manager()->get_list_of_languages();
|
||||
|
||||
$mform->addElement('header', 'badgedetails', get_string('badgedetails', 'badges'));
|
||||
$mform->addElement('text', 'name', get_string('name'), array('size' => '70'));
|
||||
@@ -61,6 +60,8 @@ class badge extends moodleform {
|
||||
$mform->addElement('text', 'version', get_string('version', 'badges'), array('size' => '70'));
|
||||
$mform->setType('version', PARAM_TEXT);
|
||||
$mform->addHelpButton('version', 'version', 'badges');
|
||||
|
||||
$languages = get_string_manager()->get_list_of_languages();
|
||||
$mform->addElement('select', 'language', get_string('language'), $languages);
|
||||
$mform->addHelpButton('language', 'language', 'badges');
|
||||
|
||||
@@ -157,7 +158,16 @@ class badge extends moodleform {
|
||||
$mform->setType('action', PARAM_TEXT);
|
||||
|
||||
if ($action == 'new') {
|
||||
$mform->setDefault('language', $CFG->lang);
|
||||
// Try to set default badge language to that of current language, or it's parent.
|
||||
$language = current_language();
|
||||
if (isset($languages[$language])) {
|
||||
$defaultlanguage = $language;
|
||||
} else {
|
||||
// Calling get_parent_language returns an empty string instead of 'en'.
|
||||
$defaultlanguage = get_parent_language($language) ?: 'en';
|
||||
}
|
||||
|
||||
$mform->setDefault('language', $defaultlanguage);
|
||||
$this->add_action_buttons(true, get_string('createbutton', 'badges'));
|
||||
} else {
|
||||
// Add hidden fields.
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Serve Issuer JSON for related badge or default Issuer if no badge is defined.
|
||||
*
|
||||
* @package core
|
||||
* @subpackage badges
|
||||
* @copyright 2020 Sara Arjona <sara@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define('AJAX_SCRIPT', true);
|
||||
define('NO_MOODLE_COOKIES', true); // No need for a session here.
|
||||
|
||||
require_once(__DIR__ . '/../config.php');
|
||||
require_once($CFG->libdir . '/badgeslib.php');
|
||||
|
||||
|
||||
$id = optional_param('id', null, PARAM_INT);
|
||||
|
||||
if (empty($id)) {
|
||||
// Get the default issuer for this site.
|
||||
$json = badges_get_default_issuer();
|
||||
} else {
|
||||
// Get the issuer for this badge.
|
||||
$badge = new badge($id);
|
||||
if ($badge->status != BADGE_STATUS_INACTIVE) {
|
||||
$json = $badge->get_badge_issuer();
|
||||
} else {
|
||||
// The badge doen't exist or not accessible for the users.
|
||||
header("HTTP/1.0 410 Gone");
|
||||
$badgeurl = new moodle_url('/badges/issuer_json.php', array('id' => $id));
|
||||
$json = ['id' => $badgeurl->out()];
|
||||
$json['error'] = get_string('error:relatedbadgedoesntexist', 'badges');
|
||||
}
|
||||
}
|
||||
|
||||
echo $OUTPUT->header();
|
||||
echo json_encode($json);
|
||||
+41
-5
@@ -114,23 +114,59 @@ class PNG_MetaDataHandler
|
||||
* @param string $value Currently an assertion URL that is added to an image metadata.
|
||||
*
|
||||
* @return string $result File content with a new chunk as a string. Can be used in file_put_contents() to write to a file.
|
||||
* @throws \moodle_exception when unsupported chunk type is defined.
|
||||
*/
|
||||
public function add_chunks($type, $key, $value) {
|
||||
if (strlen($key) > 79) {
|
||||
debugging('Key is too big');
|
||||
}
|
||||
|
||||
// tEXt Textual data.
|
||||
// Keyword: 1-79 bytes (character string)
|
||||
// Null separator: 1 byte
|
||||
// Text: n bytes (character string)
|
||||
$data = $key . "\0" . $value;
|
||||
$dataparts = [];
|
||||
if ($type === 'iTXt') {
|
||||
// International textual data (iTXt).
|
||||
// Keyword: 1-79 bytes (character string).
|
||||
$dataparts[] = $key;
|
||||
// Null separator: 1 byte.
|
||||
$dataparts[] = "\x00";
|
||||
// Compression flag: 1 byte
|
||||
// A value of 0 means no compression.
|
||||
$dataparts[] = "\x00";
|
||||
// Compression method: 1 byte
|
||||
// If compression is disabled, the method should also be 0.
|
||||
$dataparts[] = "\x00";
|
||||
// Language tag: 0 or more bytes (character string)
|
||||
// When there is no language specified leave empty.
|
||||
|
||||
// Null separator: 1 byte.
|
||||
$dataparts[] = "\x00";
|
||||
// Translated keyword: 0 or more bytes
|
||||
// When there is no translation specified, leave empty.
|
||||
|
||||
// Null separator: 1 byte.
|
||||
$dataparts[] = "\x00";
|
||||
// Text: 0 or more bytes.
|
||||
$dataparts[] = $value;
|
||||
} else if ($type === 'tEXt') {
|
||||
// Textual data (tEXt).
|
||||
// Keyword: 1-79 bytes (character string).
|
||||
$dataparts[] = $key;
|
||||
// Null separator: 1 byte.
|
||||
$dataparts[] = "\0";
|
||||
// Text: n bytes (character string).
|
||||
$dataparts[] = $value;
|
||||
} else {
|
||||
throw new \moodle_exception('Unsupported chunk type: ' . $type);
|
||||
}
|
||||
|
||||
$data = implode($dataparts);
|
||||
|
||||
$crc = pack("N", crc32($type . $data));
|
||||
$len = pack("N", strlen($data));
|
||||
|
||||
// Chunk format: length + type + data + CRC.
|
||||
// CRC is a CRC-32 computed over the chunk type and chunk data.
|
||||
$newchunk = $len . $type . $data . $crc;
|
||||
$this->_chunks[$type] = $data;
|
||||
|
||||
$result = substr($this->_contents, 0, $this->_size - 12)
|
||||
. $newchunk
|
||||
|
||||
@@ -52,8 +52,8 @@ class block_activity_modules extends block_list {
|
||||
$archetypes = array();
|
||||
|
||||
foreach($modinfo->cms as $cm) {
|
||||
// Exclude activities which are not visible or have no link (=label)
|
||||
if (!$cm->uservisible or !$cm->has_view()) {
|
||||
// Exclude activities that aren't visible or have no view link (e.g. label). Account for folder being displayed inline.
|
||||
if (!$cm->uservisible || (!$cm->has_view() && strcmp($cm->modname, 'folder') !== 0)) {
|
||||
continue;
|
||||
}
|
||||
if (array_key_exists($cm->modname, $modfullnames)) {
|
||||
|
||||
+8
-5
@@ -37,11 +37,14 @@ $string['privacy:metadata'] = 'The File cache cachestore plugin stores data brie
|
||||
$string['prescan'] = 'Prescan directory';
|
||||
$string['prescan_help'] = 'If enabled the directory is scanned when the cache is first used and requests for files are first checked against the scan data. This can help if you have a slow file system and are finding that file operations are causing you a bottle neck.';
|
||||
$string['singledirectory'] = 'Single directory store';
|
||||
$string['singledirectory_help'] = 'If enabled files (cached items) will be stored in a single directory rather than being broken up into multiple directories.<br />
|
||||
Enabling this will speed up file interactions but comes at the cost of increased risk of hitting file system limitations.<br />
|
||||
It is advisable to only turn this on if the following is true:<br />
|
||||
- If you know the number of items in the cache is going to be small enough that it won\'t cause issues on the file system you are running with.<br />
|
||||
- The data being cached is not expensive to generate. If it is then sticking with the default may still be the better option as it reduces the chance of issues.';
|
||||
$string['singledirectory_help'] = 'If enabled files (cached items) will be stored in a single directory rather than being broken up into multiple directories.
|
||||
|
||||
Enabling this will speed up file interactions but comes at the cost of increased risk of hitting file system limitations.
|
||||
|
||||
It is advisable to only turn this on if the following is true:
|
||||
|
||||
* If you know the number of items in the cache is going to be small enough that it won\'t cause issues on the file system you are running with.
|
||||
* The data being cached is not expensive to generate. If it is then sticking with the default may still be the better option as it reduces the chance of issues.';
|
||||
|
||||
/**
|
||||
* This is is like the file store, but designed for siutations where:
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
define(["jquery","core/str","core/notification","core/custom_interaction_events","core/modal","core/modal_registry","core/modal_factory","core/modal_events","core_calendar/modal_event_form","core_calendar/repository","core_calendar/events","core_calendar/modal_delete","core_calendar/selectors"],function(a,b,c,d,e,f,g,h,i,j,k,l,m){function n(d,e,f){var i=[{key:"deleteevent",component:"calendar"}];f=parseInt(f,10);var m,n=f>1;n?(i.push({key:"confirmeventseriesdelete",component:"calendar",param:{name:e,count:f}}),m=g.create({type:l.TYPE})):(i.push({key:"confirmeventdelete",component:"calendar",param:e}),m=g.create({type:g.types.SAVE_CANCEL})),m.then(function(a){a.show()}).fail(c.exception);var o=b.get_strings(i),p=a.when(o,m).then(function(b,e){return e.setTitle(b[0]),e.setBody(b[1]),n||e.setSaveButtonText(b[0]),e.getRoot().on(h.save,function(){j.deleteEvent(d,!1).then(function(){a("body").trigger(k.deleted,[d,!1])})["catch"](c.exception)}),e.getRoot().on(k.deleteAll,function(){j.deleteEvent(d,!0).then(function(){a("body").trigger(k.deleted,[d,!0])})["catch"](c.exception)}),e}).fail(c.exception);return p}function o(b){b.on("click",m.actions.remove,function(b){var c=a(this).closest(m.eventItem),d=c.data("eventId"),e=c.data("eventTitle"),f=c.data("eventCount");n(d,e,f),b.preventDefault()})}function p(b,d){return d.then(function(c){a("body").on(k.editEvent,function(a,d){var e=b.find(m.wrapper);c.setEventId(d),c.setContextId(e.data("contextId")),c.show(),a.stopImmediatePropagation()})}).fail(c.exception),d}var q=function(b){var d=g.create({type:i.TYPE,large:!0});return b.on("click",m.actions.create,function(a){d.then(function(a){var c=b.find(m.wrapper),d=c.data("categoryid");"undefined"!=typeof d&&a.setCategoryId(d);var e=b.find(m.today),f=b.find(m.day);!e.length&&f.length&&a.setStartTime(f.data("newEventTimestamp")),a.setContextId(c.data("contextId")),a.setCourseId(c.data("courseid")),a.show()}).fail(c.exception),a.preventDefault()}),b.on("click",m.actions.edit,function(b){b.preventDefault();var e=a(b.currentTarget),f=e.closest(m.wrapper),g=e.closest(m.eventItem);d.then(function(a){a.setEventId(g.data("eventId")),a.setContextId(f.data("contextId")),a.show(),b.stopImmediatePropagation()}).fail(c.exception)}),d};return{registerRemove:o,registerEditListeners:p,registerEventFormModal:q}});
|
||||
define(["jquery","core/str","core/notification","core/custom_interaction_events","core/modal","core/modal_registry","core/modal_factory","core/modal_events","core_calendar/modal_event_form","core_calendar/repository","core_calendar/events","core_calendar/modal_delete","core_calendar/selectors","core/pending"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n){function o(d,e,f){var i=[{key:"deleteevent",component:"calendar"}];f=parseInt(f,10);var m,o=f>1;o?(i.push({key:"confirmeventseriesdelete",component:"calendar",param:{name:e,count:f}}),m=g.create({type:l.TYPE})):(i.push({key:"confirmeventdelete",component:"calendar",param:e}),m=g.create({type:g.types.SAVE_CANCEL}));var p=b.get_strings(i),q=a.when(p,m).then(function(b,e){return e.setTitle(b[0]),e.setBody(b[1]),o||e.setSaveButtonText(b[0]),e.show(),e.getRoot().on(h.save,function(){var b=new n("calendar/crud:initModal:deletedevent");j.deleteEvent(d,!1).then(function(){a("body").trigger(k.deleted,[d,!1])}).then(b.resolve)["catch"](c.exception)}),e.getRoot().on(k.deleteAll,function(){var b=new n("calendar/crud:initModal:deletedallevent");j.deleteEvent(d,!0).then(function(){a("body").trigger(k.deleted,[d,!0])}).then(b.resolve)["catch"](c.exception)}),e})["catch"](c.exception);return q}function p(b){b.on("click",m.actions.remove,function(b){var c=a(this).closest(m.eventItem),d=c.data("eventId"),e=c.data("eventTitle"),f=c.data("eventCount");o(d,e,f),b.preventDefault()})}function q(b,d){return d.then(function(c){a("body").on(k.editEvent,function(a,d){var e=b.find(m.wrapper);c.setEventId(d),c.setContextId(e.data("contextId")),c.show(),a.stopImmediatePropagation()})}).fail(c.exception),d}var r=function(b){var d=g.create({type:i.TYPE,large:!0});return b.on("click",m.actions.create,function(a){d.then(function(a){var c=b.find(m.wrapper),d=c.data("categoryid");"undefined"!=typeof d&&a.setCategoryId(d);var e=b.find(m.today),f=b.find(m.day);!e.length&&f.length&&a.setStartTime(f.data("newEventTimestamp")),a.setContextId(c.data("contextId")),a.setCourseId(c.data("courseid")),a.show()}).fail(c.exception),a.preventDefault()}),b.on("click",m.actions.edit,function(b){b.preventDefault();var e=a(b.currentTarget),f=e.closest(m.wrapper),g=e.closest(m.eventItem);d.then(function(a){a.setEventId(g.data("eventId")),a.setContextId(f.data("contextId")),a.show(),b.stopImmediatePropagation()}).fail(c.exception)}),d};return{registerRemove:p,registerEditListeners:q,registerEventFormModal:r}});
|
||||
@@ -20,7 +20,7 @@
|
||||
* components by listening for and responding to different events
|
||||
* triggered within the calendar UI.
|
||||
*
|
||||
* @module core_calendar/calendar
|
||||
* @module core_calendar/calendar_mini
|
||||
* @package core_calendar
|
||||
* @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
|
||||
@@ -35,6 +35,7 @@ define([
|
||||
'core_calendar/events',
|
||||
'core_calendar/modal_delete',
|
||||
'core_calendar/selectors',
|
||||
'core/pending',
|
||||
],
|
||||
function(
|
||||
$,
|
||||
@@ -49,7 +50,8 @@ function(
|
||||
CalendarRepository,
|
||||
CalendarEvents,
|
||||
ModalDelete,
|
||||
CalendarSelectors
|
||||
CalendarSelectors,
|
||||
Pending
|
||||
) {
|
||||
|
||||
/**
|
||||
@@ -101,13 +103,6 @@ function(
|
||||
);
|
||||
}
|
||||
|
||||
deletePromise.then(function(deleteModal) {
|
||||
deleteModal.show();
|
||||
|
||||
return;
|
||||
})
|
||||
.fail(Notification.exception);
|
||||
|
||||
var stringsPromise = Str.get_strings(deleteStrings);
|
||||
|
||||
var finalPromise = $.when(stringsPromise, deletePromise)
|
||||
@@ -118,27 +113,33 @@ function(
|
||||
deleteModal.setSaveButtonText(strings[0]);
|
||||
}
|
||||
|
||||
deleteModal.show();
|
||||
|
||||
deleteModal.getRoot().on(ModalEvents.save, function() {
|
||||
var pendingPromise = new Pending('calendar/crud:initModal:deletedevent');
|
||||
CalendarRepository.deleteEvent(eventId, false)
|
||||
.then(function() {
|
||||
$('body').trigger(CalendarEvents.deleted, [eventId, false]);
|
||||
return;
|
||||
})
|
||||
.then(pendingPromise.resolve)
|
||||
.catch(Notification.exception);
|
||||
});
|
||||
|
||||
deleteModal.getRoot().on(CalendarEvents.deleteAll, function() {
|
||||
var pendingPromise = new Pending('calendar/crud:initModal:deletedallevent');
|
||||
CalendarRepository.deleteEvent(eventId, true)
|
||||
.then(function() {
|
||||
$('body').trigger(CalendarEvents.deleted, [eventId, true]);
|
||||
return;
|
||||
})
|
||||
.then(pendingPromise.resolve)
|
||||
.catch(Notification.exception);
|
||||
});
|
||||
|
||||
return deleteModal;
|
||||
})
|
||||
.fail(Notification.exception);
|
||||
.catch(Notification.exception);
|
||||
|
||||
return finalPromise;
|
||||
}
|
||||
|
||||
+26
@@ -126,6 +126,10 @@ class day_exporter extends exporter {
|
||||
'type' => PARAM_URL,
|
||||
'optional' => true,
|
||||
],
|
||||
'viewdaylinktitle' => [
|
||||
'type' => PARAM_RAW,
|
||||
'optional' => true,
|
||||
],
|
||||
'events' => [
|
||||
'type' => calendar_event_exporter::read_properties_definition(),
|
||||
'multiple' => true,
|
||||
@@ -183,6 +187,10 @@ class day_exporter extends exporter {
|
||||
'viewdaylink' => $this->url->out(false),
|
||||
];
|
||||
|
||||
if ($viewdaylinktitle = $this->get_view_link_title()) {
|
||||
$return['viewdaylinktitle'] = $viewdaylinktitle;
|
||||
}
|
||||
|
||||
|
||||
$cache = $this->related['cache'];
|
||||
$eventexporters = array_map(function($event) use ($cache, $output) {
|
||||
@@ -267,4 +275,22 @@ class day_exporter extends exporter {
|
||||
'time' => $this->calendar->time,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the title for view link.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get_view_link_title() {
|
||||
$title = null;
|
||||
|
||||
$userdate = userdate($this->data[0], get_string('strftimedayshort'));
|
||||
if ($this->data['istoday']) {
|
||||
$title = get_string('todayplustitle', 'calendar', $userdate);
|
||||
} else if (count($this->related['events'])) {
|
||||
$title = get_string('eventsfor', 'calendar', $userdate);
|
||||
}
|
||||
|
||||
return $title;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,12 +292,16 @@ class core_calendar_renderer extends plugin_renderer_base {
|
||||
$courseurl = new moodle_url($returnurl);
|
||||
$courseurl->remove_params('course');
|
||||
|
||||
if ($label === null) {
|
||||
if (empty($label)) {
|
||||
$label = get_string('listofcourses');
|
||||
$labelclass = 'sr-only';
|
||||
} else {
|
||||
$labelclass = 'mr-1';
|
||||
}
|
||||
|
||||
$select = html_writer::label($label, 'course', false, ['class' => 'mr-1']);
|
||||
$select .= html_writer::select($courseoptions, 'course', $selected, false, ['class' => 'cal_courses_flt']);
|
||||
$select = html_writer::label($label, 'course', false, ['class' => $labelclass]);
|
||||
$select .= html_writer::select($courseoptions, 'course', $selected, false,
|
||||
['class' => 'cal_courses_flt', 'id' => 'course']);
|
||||
|
||||
return $select;
|
||||
}
|
||||
|
||||
@@ -33,10 +33,11 @@
|
||||
}} data-container="body"{{!
|
||||
}} data-toggle="popover"{{!
|
||||
}} data-html="true"{{!
|
||||
}} data-trigger="hover"{{!
|
||||
}} data-trigger="hover focus"{{!
|
||||
}} data-placement="top"{{!
|
||||
}} data-title="{{$title}}{{title}}{{/title}}"{{!
|
||||
}} data-alternate="{{$nocontent}}{{/nocontent}}"{{!
|
||||
}} aria-label="{{viewdaylinktitle}}"{{!
|
||||
}}>{{$day}}{{day}}{{/day}}</a>
|
||||
<div class="hidden">
|
||||
{{$content}}{{/content}}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
{{# daynames }}
|
||||
<th class="header text-xs-center">
|
||||
<th class="header text-xs-center" aria-label="{{fullname}}">
|
||||
{{shortname}}
|
||||
</th>
|
||||
{{/ daynames }}
|
||||
@@ -73,7 +73,7 @@
|
||||
data-new-event-timestamp="{{neweventtimestamp}}">
|
||||
<div class="d-none d-md-block hidden-phone text-xs-center">
|
||||
{{#hasevents}}
|
||||
<a data-action="view-day-link" href="{{viewdaylink}}" class="day" title="{{viewdaylinktitle}}">{{mday}}</a>
|
||||
<a data-action="view-day-link" href="{{viewdaylink}}" class="day" aria-label="{{viewdaylinktitle}}">{{mday}}</a>
|
||||
{{/hasevents}}
|
||||
{{^hasevents}}
|
||||
{{mday}}
|
||||
@@ -121,12 +121,10 @@
|
||||
</div>
|
||||
<div class="d-md-none hidden-desktop hidden-tablet">
|
||||
{{#hasevents}}
|
||||
<a data-action="view-day-link" href="{{viewdaylink}}" class="day" title="{{viewdaylinktitle}}">{{mday}}</a>
|
||||
<a data-action="view-day-link" href="{{viewdaylink}}" class="day" aria-label="{{viewdaylinktitle}}">{{mday}}</a>
|
||||
{{/hasevents}}
|
||||
{{^hasevents}}
|
||||
<div data-region="day-content">
|
||||
{{mday}}
|
||||
</div>
|
||||
{{/hasevents}}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -77,8 +77,8 @@
|
||||
<thead>
|
||||
<tr>
|
||||
{{# daynames }}
|
||||
<th class="header text-xs-center" scope="col">
|
||||
<abbr title="{{fullname}}">{{shortname}}</abbr>
|
||||
<th class="header text-xs-center" scope="col" aria-label="{{fullname}}">
|
||||
{{shortname}}
|
||||
</th>
|
||||
{{/ daynames }}
|
||||
</tr>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}}
|
||||
{{!
|
||||
@template calendar/calendar_threemonth
|
||||
@template calendar/threemonth_month
|
||||
|
||||
Calendar view to show three months as a block.
|
||||
|
||||
|
||||
@@ -52,11 +52,23 @@ require_once($CFG->dirroot.'/calendar/lib.php');
|
||||
$categoryid = optional_param('category', null, PARAM_INT);
|
||||
$courseid = optional_param('course', SITEID, PARAM_INT);
|
||||
$view = optional_param('view', 'upcoming', PARAM_ALPHA);
|
||||
$day = optional_param('cal_d', 0, PARAM_INT);
|
||||
$mon = optional_param('cal_m', 0, PARAM_INT);
|
||||
$year = optional_param('cal_y', 0, PARAM_INT);
|
||||
$time = optional_param('time', 0, PARAM_INT);
|
||||
$lookahead = optional_param('lookahead', null, PARAM_INT);
|
||||
|
||||
$url = new moodle_url('/calendar/view.php');
|
||||
|
||||
// If a day, month and year were passed then convert it to a timestamp. If these were passed
|
||||
// then we can assume the day, month and year are passed as Gregorian, as no where in core
|
||||
// should we be passing these values rather than the time. This is done for BC.
|
||||
if (!empty($day) && !empty($mon) && !empty($year)) {
|
||||
if (checkdate($mon, $day, $year)) {
|
||||
$time = make_timestamp($year, $mon, $day);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($time)) {
|
||||
$time = time();
|
||||
}
|
||||
|
||||
@@ -4777,6 +4777,40 @@ class api {
|
||||
$DB->delete_records(template_cohort::TABLE, array('cohortid' => $cohort->id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to perform when a user is deleted.
|
||||
*
|
||||
* @param int $userid The user id.
|
||||
*/
|
||||
public static function hook_user_deleted($userid) {
|
||||
global $DB;
|
||||
|
||||
$usercompetencies = $DB->get_records(user_competency::TABLE, ['userid' => $userid], '', 'id');
|
||||
foreach ($usercompetencies as $usercomp) {
|
||||
$DB->delete_records(evidence::TABLE, ['usercompetencyid' => $usercomp->id]);
|
||||
}
|
||||
|
||||
$DB->delete_records(user_competency::TABLE, ['userid' => $userid]);
|
||||
$DB->delete_records(user_competency_course::TABLE, ['userid' => $userid]);
|
||||
$DB->delete_records(user_competency_plan::TABLE, ['userid' => $userid]);
|
||||
|
||||
// Delete any associated files.
|
||||
$fs = get_file_storage();
|
||||
$context = context_user::instance($userid);
|
||||
$userevidences = $DB->get_records(user_evidence::TABLE, ['userid' => $userid], '', 'id');
|
||||
foreach ($userevidences as $userevidence) {
|
||||
$DB->delete_records(user_evidence_competency::TABLE, ['userevidenceid' => $userevidence->id]);
|
||||
$DB->delete_records(user_evidence::TABLE, ['id' => $userevidence->id]);
|
||||
$fs->delete_area_files($context->id, 'core_competency', 'userevidence', $userevidence->id);
|
||||
}
|
||||
|
||||
$userplans = $DB->get_records(plan::TABLE, ['userid' => $userid], '', 'id');
|
||||
foreach ($userplans as $userplan) {
|
||||
$DB->delete_records(plan_competency::TABLE, ['planid' => $userplan->id]);
|
||||
$DB->delete_records(plan::TABLE, ['id' => $userplan->id]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually grade a user competency.
|
||||
*
|
||||
|
||||
@@ -207,4 +207,47 @@ class core_competency_hooks_testcase extends advanced_testcase {
|
||||
$this->assertEquals(1, \core_competency\template_cohort::count_records(array('templateid' => $t1->get('id'))));
|
||||
$this->assertEquals(0, \core_competency\template_cohort::count_records(array('templateid' => $t2->get('id'))));
|
||||
}
|
||||
|
||||
public function test_hook_user_deleted() {
|
||||
$this->resetAfterTest();
|
||||
$dg = $this->getDataGenerator();
|
||||
$ccg = $dg->get_plugin_generator('core_competency');
|
||||
|
||||
$u1 = $dg->create_user();
|
||||
|
||||
$framework = $ccg->create_framework();
|
||||
$comp1 = $ccg->create_competency(['competencyframeworkid' => $framework->get('id')]);
|
||||
$comp2 = $ccg->create_competency(['competencyframeworkid' => $framework->get('id')]);
|
||||
|
||||
$c1 = $dg->create_course();
|
||||
$cc1a = $ccg->create_course_competency(['competencyid' => $comp1->get('id'), 'courseid' => $c1->id]);
|
||||
$cc1b = $ccg->create_course_competency(['competencyid' => $comp2->get('id'), 'courseid' => $c1->id]);
|
||||
$assign1a = $dg->create_module('assign', ['course' => $c1]);
|
||||
$assign1b = $dg->create_module('assign', ['course' => $c1]);
|
||||
$cmc1a = $ccg->create_course_module_competency(['competencyid' => $comp1->get('id'), 'cmid' => $assign1a->cmid]);
|
||||
$cmc1b = $ccg->create_course_module_competency(['competencyid' => $comp1->get('id'), 'cmid' => $assign1b->cmid]);
|
||||
$ucc1a = $ccg->create_user_competency_course(['competencyid' => $comp1->get('id'), 'courseid' => $c1->id,
|
||||
'userid' => $u1->id]);
|
||||
$ucc1b = $ccg->create_user_competency_course(['competencyid' => $comp2->get('id'), 'courseid' => $c1->id,
|
||||
'userid' => $u1->id]);
|
||||
|
||||
$c2 = $dg->create_course();
|
||||
$cc2a = $ccg->create_course_competency(['competencyid' => $comp1->get('id'), 'courseid' => $c2->id]);
|
||||
$cc2b = $ccg->create_course_competency(['competencyid' => $comp2->get('id'), 'courseid' => $c2->id]);
|
||||
$assign2a = $dg->create_module('assign', ['course' => $c2]);
|
||||
$assign2b = $dg->create_module('assign', ['course' => $c2]);
|
||||
$cmc2a = $ccg->create_course_module_competency(['competencyid' => $comp1->get('id'), 'cmid' => $assign2a->cmid]);
|
||||
$cmc2b = $ccg->create_course_module_competency(['competencyid' => $comp1->get('id'), 'cmid' => $assign2b->cmid]);
|
||||
$ucc2a = $ccg->create_user_competency_course(['competencyid' => $comp1->get('id'), 'courseid' => $c2->id,
|
||||
'userid' => $u1->id]);
|
||||
$ucc2b = $ccg->create_user_competency_course(['competencyid' => $comp2->get('id'), 'courseid' => $c2->id,
|
||||
'userid' => $u1->id]);
|
||||
|
||||
reset_course_userdata((object) ['id' => $c1->id, 'reset_competency_ratings' => true]);
|
||||
|
||||
delete_user($u1);
|
||||
|
||||
// Assert the records don't exist anymore.
|
||||
$this->assertEquals(0, user_competency_course::count_records(['courseid' => $c1->id, 'userid' => $u1->id]));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "7.5.*",
|
||||
"phpunit/dbunit": "4.0.*",
|
||||
"moodlehq/behat-extension": "3.37.3",
|
||||
"moodlehq/behat-extension": "3.37.4",
|
||||
"mikey179/vfsstream": "^1.6",
|
||||
"instaclick/php-webdriver": "dev-local as 1.x-dev"
|
||||
}
|
||||
|
||||
Generated
+756
-402
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
||||
define(["jquery"],function(a){var b=function(b){var d=a(this);if("sectionname"===d.data("itemtype")){var e=M.course.format.get_config(),f=d.closest("."+e.section_class);c(f,b.ajaxreturn.value)}},c=function(a,b){a.attr("aria-label",b),a.find(".hidden.sectionname").text(b)};a("body").on("updated",".inplaceeditable",b)});
|
||||
@@ -569,7 +569,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
|
||||
str.get_string('numberweeks').done(function(strNumberSections) {
|
||||
var trigger = $(SELECTOR.ADDSECTIONS),
|
||||
modalTitle = trigger.attr('data-add-sections'),
|
||||
newSections = trigger.attr('new-sections');
|
||||
newSections = trigger.attr('data-new-sections');
|
||||
var modalBody = $('<div><label for="add_section_numsections"></label> ' +
|
||||
'<input id="add_section_numsections" type="number" min="1" max="' + newSections + '" value="1"></div>');
|
||||
modalBody.find('label').html(strNumberSections);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* Define listeners related to sections.
|
||||
*
|
||||
* @module core_course/sectionlistener
|
||||
* @package core_course
|
||||
* @copyright 2020 University of Nottingham
|
||||
* @author Neill Magill <neill.magill@nottingham.ac.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define(['jquery'], function($) {
|
||||
/**
|
||||
* Processes an updated event.
|
||||
*
|
||||
* We only want to handle events related to a section, and not activities.
|
||||
*
|
||||
* @param {Event} e The updated event
|
||||
*/
|
||||
var processUpdatedEvent = function(e) {
|
||||
var editable = $(this);
|
||||
if (editable.data('itemtype') === 'sectionname') {
|
||||
var config = M.course.format.get_config();
|
||||
// A section name got updated.
|
||||
var section = editable.closest('.' + config.section_class);
|
||||
updateSectionName(section, e.ajaxreturn.value);
|
||||
}
|
||||
// Ignore other types of inplace updates, for example activity name changes.
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the name of the section in places outside of the inplace editable field.
|
||||
*
|
||||
* @param {jQuery} section The section that was updated.
|
||||
* @param {String} newname The new name for the section.
|
||||
*/
|
||||
var updateSectionName = function(section, newname) {
|
||||
section.attr('aria-label', newname);
|
||||
section.find('.hidden.sectionname').text(newname);
|
||||
};
|
||||
|
||||
$('body').on('updated', '.inplaceeditable', processUpdatedEvent);
|
||||
});
|
||||
@@ -717,13 +717,49 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
|
||||
* @return mixed
|
||||
*/
|
||||
protected static function get_tree($id) {
|
||||
global $DB;
|
||||
$coursecattreecache = cache::make('core', 'coursecattree');
|
||||
$rv = $coursecattreecache->get($id);
|
||||
if ($rv !== false) {
|
||||
return $rv;
|
||||
}
|
||||
// Might need to rebuild the tree. Put a lock in place to ensure other requests don't try and do this in parallel.
|
||||
$lockfactory = \core\lock\lock_config::get_lock_factory('core_coursecattree');
|
||||
$lock = $lockfactory->get_lock('core_coursecattree_cache',
|
||||
course_modinfo::COURSE_CACHE_LOCK_WAIT, course_modinfo::COURSE_CACHE_LOCK_EXPIRY);
|
||||
if ($lock === false) {
|
||||
// Couldn't get a lock to rebuild the tree.
|
||||
return [];
|
||||
}
|
||||
$rv = $coursecattreecache->get($id);
|
||||
if ($rv !== false) {
|
||||
// Tree was built while we were waiting for the lock.
|
||||
$lock->release();
|
||||
return $rv;
|
||||
}
|
||||
// Re-build the tree.
|
||||
try {
|
||||
$all = self::rebuild_coursecattree_cache_contents();
|
||||
$coursecattreecache->set_many($all);
|
||||
} finally {
|
||||
$lock->release();
|
||||
}
|
||||
if (array_key_exists($id, $all)) {
|
||||
return $all[$id];
|
||||
}
|
||||
// Requested non-existing category.
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the course category tree as an array, including an extra "countall" field.
|
||||
*
|
||||
* @return array
|
||||
* @throws coding_exception
|
||||
* @throws dml_exception
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
private static function rebuild_coursecattree_cache_contents() : array {
|
||||
global $DB;
|
||||
$sql = "SELECT cc.id, cc.parent, cc.visible
|
||||
FROM {course_categories} cc
|
||||
ORDER BY cc.sortorder";
|
||||
@@ -760,12 +796,7 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
|
||||
}
|
||||
// We must add countall to all in case it was the requested ID.
|
||||
$all['countall'] = $count;
|
||||
$coursecattreecache->set_many($all);
|
||||
if (array_key_exists($id, $all)) {
|
||||
return $all[$id];
|
||||
}
|
||||
// Requested non-existing category.
|
||||
return array();
|
||||
return $all;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -68,6 +68,17 @@ class course_handler extends \core_customfield\handler {
|
||||
return self::$singleton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run reset code after unit tests to reset the singleton usage.
|
||||
*/
|
||||
public static function reset_caches(): void {
|
||||
if (!PHPUNIT_TEST) {
|
||||
throw new \coding_exception('This feature is only intended for use in unit tests');
|
||||
}
|
||||
|
||||
static::$singleton = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current user can configure custom fields on this component.
|
||||
*
|
||||
|
||||
@@ -330,6 +330,26 @@ class helper {
|
||||
'string' => new \lang_string('restorecourse', 'admin')
|
||||
);
|
||||
}
|
||||
// Recyclebyn.
|
||||
if (\tool_recyclebin\category_bin::is_enabled()) {
|
||||
$categorybin = new \tool_recyclebin\category_bin($category->id);
|
||||
if ($categorybin->can_view()) {
|
||||
$autohide = get_config('tool_recyclebin', 'autohide');
|
||||
if ($autohide) {
|
||||
$items = $categorybin->get_items();
|
||||
} else {
|
||||
$items = [];
|
||||
}
|
||||
if (!$autohide || !empty($items)) {
|
||||
$pluginname = get_string('pluginname', 'tool_recyclebin');
|
||||
$actions['recyclebin'] = [
|
||||
'url' => new \moodle_url('/admin/tool/recyclebin/index.php', ['contextid' => $category->get_context()->id]),
|
||||
'icon' => new \pix_icon('trash', $pluginname, 'tool_recyclebin'),
|
||||
'string' => $pluginname
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
@@ -208,17 +208,18 @@ class core_course_management_renderer extends plugin_renderer_base {
|
||||
}
|
||||
$courseicon = $this->output->pix_icon('i/course', get_string('courses'));
|
||||
$bcatinput = array(
|
||||
'id' => 'categorylistitem' . $category->id,
|
||||
'type' => 'checkbox',
|
||||
'name' => 'bcat[]',
|
||||
'value' => $category->id,
|
||||
'class' => 'bulk-action-checkbox',
|
||||
'aria-label' => get_string('bulkactionselect', 'moodle', $text),
|
||||
'class' => 'bulk-action-checkbox custom-control-input',
|
||||
'data-action' => 'select'
|
||||
);
|
||||
|
||||
$checkboxclass = '';
|
||||
if (!$category->can_resort_subcategories() && !$category->has_manage_capability()) {
|
||||
// Very very hardcoded here.
|
||||
$bcatinput['style'] = 'visibility:hidden';
|
||||
$checkboxclass = 'd-none';
|
||||
}
|
||||
|
||||
$viewcaturl = new moodle_url('/course/management.php', array('categoryid' => $category->id));
|
||||
@@ -260,8 +261,14 @@ class core_course_management_renderer extends plugin_renderer_base {
|
||||
|
||||
$html = html_writer::start_tag('li', $attributes);
|
||||
$html .= html_writer::start_div('clearfix');
|
||||
$html .= html_writer::start_div('float-left ba-checkbox');
|
||||
$html .= html_writer::empty_tag('input', $bcatinput).' ';
|
||||
$html .= html_writer::start_div('float-left ' . $checkboxclass);
|
||||
$html .= html_writer::start_div('custom-control custom-checkbox mr-1 ');
|
||||
$html .= html_writer::empty_tag('input', $bcatinput);
|
||||
$html .= html_writer::tag('label', '', array(
|
||||
'aria-label' => get_string('bulkactionselect', 'moodle', $text),
|
||||
'class' => 'custom-control-label',
|
||||
'for' => 'categorylistitem' . $category->id));
|
||||
$html .= html_writer::end_div();
|
||||
$html .= html_writer::end_div();
|
||||
$html .= $icon;
|
||||
if ($hasactions) {
|
||||
@@ -275,7 +282,7 @@ class core_course_management_renderer extends plugin_renderer_base {
|
||||
$html .= html_writer::link($viewcaturl, $text, $textattributes);
|
||||
$html .= html_writer::start_div('float-right d-flex');
|
||||
if ($category->idnumber) {
|
||||
$html .= html_writer::tag('span', s($category->idnumber), array('class' => 'dimmed idnumber'));
|
||||
$html .= html_writer::tag('span', s($category->idnumber), array('class' => 'text-muted idnumber'));
|
||||
}
|
||||
if ($hasactions) {
|
||||
$html .= $this->category_listitem_actions($category, $actions);
|
||||
@@ -285,7 +292,7 @@ class core_course_management_renderer extends plugin_renderer_base {
|
||||
html_writer::span($category->get_courses_count()) .
|
||||
html_writer::span(get_string('courses'), 'accesshide', array('id' => $countid)) .
|
||||
$courseicon,
|
||||
'course-count dimmed',
|
||||
'course-count text-muted',
|
||||
array('aria-labelledby' => $countid)
|
||||
);
|
||||
$html .= html_writer::end_div();
|
||||
@@ -574,7 +581,7 @@ class core_course_management_renderer extends plugin_renderer_base {
|
||||
$a->total = $totalcourses;
|
||||
$str = get_string('showingxofycourses', 'moodle', $a);
|
||||
}
|
||||
$html .= html_writer::div($str, 'listing-pagination-totals dimmed');
|
||||
$html .= html_writer::div($str, 'listing-pagination-totals text-muted');
|
||||
}
|
||||
|
||||
if ($viewmode !== 'default') {
|
||||
@@ -609,16 +616,18 @@ class core_course_management_renderer extends plugin_renderer_base {
|
||||
);
|
||||
|
||||
$bulkcourseinput = array(
|
||||
'id' => 'courselistitem' . $course->id,
|
||||
'type' => 'checkbox',
|
||||
'name' => 'bc[]',
|
||||
'value' => $course->id,
|
||||
'class' => 'bulk-action-checkbox',
|
||||
'aria-label' => get_string('bulkactionselect', 'moodle', $text),
|
||||
'class' => 'bulk-action-checkbox custom-control-input',
|
||||
'data-action' => 'select'
|
||||
);
|
||||
|
||||
$checkboxclass = '';
|
||||
if (!$category->has_manage_capability()) {
|
||||
// Very very hardcoded here.
|
||||
$bulkcourseinput['style'] = 'visibility:hidden';
|
||||
$checkboxclass = 'd-none';
|
||||
}
|
||||
|
||||
$viewcourseurl = new moodle_url($this->page->url, array('courseid' => $course->id));
|
||||
@@ -631,13 +640,19 @@ class core_course_management_renderer extends plugin_renderer_base {
|
||||
$html .= html_writer::div($this->output->pix_icon('i/move_2d', get_string('dndcourse')), 'float-left drag-handle');
|
||||
}
|
||||
|
||||
$html .= html_writer::start_div('ba-checkbox float-left');
|
||||
$html .= html_writer::empty_tag('input', $bulkcourseinput).' ';
|
||||
$html .= html_writer::start_div('float-left ' . $checkboxclass);
|
||||
$html .= html_writer::start_div('custom-control custom-checkbox mr-1 ');
|
||||
$html .= html_writer::empty_tag('input', $bulkcourseinput);
|
||||
$html .= html_writer::tag('label', '', array(
|
||||
'aria-label' => get_string('bulkactionselect', 'moodle', $text),
|
||||
'class' => 'custom-control-label',
|
||||
'for' => 'courselistitem' . $course->id));
|
||||
$html .= html_writer::end_div();
|
||||
$html .= html_writer::end_div();
|
||||
$html .= html_writer::link($viewcourseurl, $text, array('class' => 'float-left coursename'));
|
||||
$html .= html_writer::start_div('float-right');
|
||||
if ($course->idnumber) {
|
||||
$html .= html_writer::tag('span', s($course->idnumber), array('class' => 'dimmed idnumber'));
|
||||
$html .= html_writer::tag('span', s($course->idnumber), array('class' => 'text-muted idnumber'));
|
||||
}
|
||||
$html .= $this->course_listitem_actions($category, $course);
|
||||
$html .= html_writer::end_div();
|
||||
@@ -1123,7 +1138,7 @@ class core_course_management_renderer extends plugin_renderer_base {
|
||||
$a->total = $totalcourses;
|
||||
$str = get_string('showingxofycourses', 'moodle', $a);
|
||||
}
|
||||
$html .= html_writer::div($str, 'listing-pagination-totals dimmed');
|
||||
$html .= html_writer::div($str, 'listing-pagination-totals text-muted');
|
||||
}
|
||||
|
||||
if ($totalcourses < $perpage) {
|
||||
@@ -1186,10 +1201,10 @@ class core_course_management_renderer extends plugin_renderer_base {
|
||||
if (core_course_category::get($course->category)->can_move_courses_out_of()) {
|
||||
$bulkcourseinput = array(
|
||||
'type' => 'checkbox',
|
||||
'id' => 'coursesearchlistitem' . $course->id,
|
||||
'name' => 'bc[]',
|
||||
'value' => $course->id,
|
||||
'class' => 'bulk-action-checkbox',
|
||||
'aria-label' => get_string('bulkactionselect', 'moodle', $text),
|
||||
'class' => 'bulk-action-checkbox custom-control-input',
|
||||
'data-action' => 'select'
|
||||
);
|
||||
}
|
||||
@@ -1200,14 +1215,20 @@ class core_course_management_renderer extends plugin_renderer_base {
|
||||
$html .= html_writer::start_div('clearfix');
|
||||
$html .= html_writer::start_div('float-left');
|
||||
if ($bulkcourseinput) {
|
||||
$html .= html_writer::empty_tag('input', $bulkcourseinput).' ';
|
||||
$html .= html_writer::start_div('custom-control custom-checkbox mr-1');
|
||||
$html .= html_writer::empty_tag('input', $bulkcourseinput);
|
||||
$html .= html_writer::tag('label', '', array(
|
||||
'aria-label' => get_string('bulkactionselect', 'moodle', $text),
|
||||
'class' => 'custom-control-label',
|
||||
'for' => 'coursesearchlistitem' . $course->id));
|
||||
$html .= html_writer::end_div();
|
||||
}
|
||||
$html .= html_writer::end_div();
|
||||
$html .= html_writer::link($viewcourseurl, $text, array('class' => 'float-left coursename'));
|
||||
$html .= html_writer::tag('span', $categoryname, array('class' => 'float-left categoryname'));
|
||||
$html .= html_writer::tag('span', $categoryname, array('class' => 'float-left ml-3 text-muted'));
|
||||
$html .= html_writer::start_div('float-right');
|
||||
$html .= $this->search_listitem_actions($course);
|
||||
$html .= html_writer::tag('span', s($course->idnumber), array('class' => 'dimmed idnumber'));
|
||||
$html .= html_writer::tag('span', s($course->idnumber), array('class' => 'text-muted idnumber'));
|
||||
$html .= html_writer::end_div();
|
||||
$html .= html_writer::end_div();
|
||||
$html .= html_writer::end_tag('li');
|
||||
|
||||
@@ -514,7 +514,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
|
||||
|
||||
// Output section activities summary:
|
||||
$o = '';
|
||||
$o.= html_writer::start_tag('div', array('class' => 'section-summary-activities mdl-right'));
|
||||
$o.= html_writer::start_tag('div', array('class' => 'section-summary-activities pr-2 mdl-right'));
|
||||
foreach ($sectionmods as $mod) {
|
||||
$o.= html_writer::start_tag('span', array('class' => 'activity-count'));
|
||||
$o.= $mod['name'].': '.$mod['count'];
|
||||
@@ -528,7 +528,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
|
||||
$a->complete = $complete;
|
||||
$a->total = $total;
|
||||
|
||||
$o.= html_writer::start_tag('div', array('class' => 'section-summary-activities mdl-right'));
|
||||
$o.= html_writer::start_tag('div', array('class' => 'section-summary-activities pr-2 mdl-right'));
|
||||
$o.= html_writer::tag('span', get_string('progresstotal', 'completion', $a), array('class' => 'activity-count'));
|
||||
$o.= html_writer::end_tag('div');
|
||||
}
|
||||
@@ -1006,7 +1006,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
|
||||
$icon = $this->output->pix_icon('t/add', '');
|
||||
$newsections = $maxsections - $lastsection;
|
||||
echo html_writer::link($url, $icon . $straddsections,
|
||||
array('class' => 'add-sections', 'data-add-sections' => $straddsections, 'new-sections' => $newsections));
|
||||
array('class' => 'add-sections', 'data-add-sections' => $straddsections, 'data-new-sections' => $newsections));
|
||||
echo html_writer::end_tag('div');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,9 +154,9 @@ class format_singleactivity extends format_base {
|
||||
|
||||
if ($fetchtypes) {
|
||||
$availabletypes = $this->get_supported_activities();
|
||||
if ($this->course) {
|
||||
if ($this->courseid) {
|
||||
// The course exists. Test against the course.
|
||||
$testcontext = context_course::instance($this->course->id);
|
||||
$testcontext = context_course::instance($this->courseid);
|
||||
} else if ($this->categoryid) {
|
||||
// The course does not exist yet, but we have a category ID that we can test against.
|
||||
$testcontext = context_coursecat::instance($this->categoryid);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
@format @format_singleactivity
|
||||
Feature: Edit format course to Single Activity format
|
||||
In order to set the format course to single activity course
|
||||
As a teacher
|
||||
I need to edit the course settings and see the dropdown type activity
|
||||
|
||||
Scenario: Edit a format course as a teacher
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | summary | format |
|
||||
| Course 1 | C1 | <p>Course summary</p> | topics |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
When I navigate to "Edit settings" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Course full name | My first course |
|
||||
| Course short name | myfirstcourse |
|
||||
| Format | Single activity format |
|
||||
And I press "Update format"
|
||||
Then I should see "Forum" in the "Type of activity" "field"
|
||||
And I press "Save and display"
|
||||
And I should see "Adding a new Forum"
|
||||
@@ -48,6 +48,7 @@ M.course.format.swap_sections = function(Y, node1, node2) {
|
||||
* Process sections after ajax response
|
||||
*
|
||||
* @param {YUI} Y YUI3 instance
|
||||
* @param {NodeList} sectionlist of sections
|
||||
* @param {array} response ajax response
|
||||
* @param {string} sectionfrom first affected section
|
||||
* @param {string} sectionto last affected section
|
||||
@@ -76,13 +77,14 @@ M.course.format.process_sections = function(Y, sectionlist, response, sectionfro
|
||||
// Update section title.
|
||||
var content = Y.Node.create('<span>' + response.sectiontitles[i] + '</span>');
|
||||
sectionlist.item(i).all('.'+CSS.SECTIONNAME).setHTML(content);
|
||||
// Update move icon.
|
||||
ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE);
|
||||
str = ele.getAttribute('alt');
|
||||
// Update the drag handle.
|
||||
ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE).ancestor('.section-handle');
|
||||
str = ele.getAttribute('title');
|
||||
stridx = str.lastIndexOf(' ');
|
||||
newstr = str.substr(0, stridx +1) + i;
|
||||
ele.setAttribute('alt', newstr);
|
||||
ele.setAttribute('title', newstr); // For FireFox as 'alt' is not refreshed.
|
||||
ele.setAttribute('title', newstr);
|
||||
// Update the aria-label for the section.
|
||||
sectionlist.item(i).setAttribute('aria-label', content.get('innerText').trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,3 +59,4 @@ if (!empty($displaysection)) {
|
||||
|
||||
// Include course format js module
|
||||
$PAGE->requires->js('/course/format/topics/format.js');
|
||||
$PAGE->requires->js_call_amd('core_course/sectionlistener');
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
.course-content ul.topics {
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.course-content ul.topics li.section .content {
|
||||
margin: 0 40px;
|
||||
}
|
||||
|
||||
.course-content ul.topics li.section .left,
|
||||
.course-content ul.topics li.section .right {
|
||||
width: 40px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.course-content ul.topics li.section .left {
|
||||
padding-top: 22px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.jsenabled .course-content ul.topics li.section .left,
|
||||
.jsenabled .course-content ul.topics li.section .right {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.course-content ul.topics li.section .left .section-handle img.icon {
|
||||
padding: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.course-content ul.topics li.section .section_action_menu .textmenu,
|
||||
.course-content ul.topics li.section .section_action_menu .menu-action-text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -48,6 +48,7 @@ M.course.format.swap_sections = function(Y, node1, node2) {
|
||||
* Process sections after ajax response
|
||||
*
|
||||
* @param {YUI} Y YUI3 instance
|
||||
* @param {NodeList} sectionlist of sections
|
||||
* @param {array} response ajax response
|
||||
* @param {string} sectionfrom first affected section
|
||||
* @param {string} sectionto last affected section
|
||||
@@ -77,13 +78,14 @@ M.course.format.process_sections = function(Y, sectionlist, response, sectionfro
|
||||
var content = Y.Node.create('<span>' + response.sectiontitles[i] + '</span>');
|
||||
sectionlist.item(i).all('.'+CSS.SECTIONNAME).setHTML(content);
|
||||
|
||||
// Update move icon.
|
||||
ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE);
|
||||
str = ele.getAttribute('alt');
|
||||
// Update the drag handle.
|
||||
ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE).ancestor('.section-handle');
|
||||
str = ele.getAttribute('title');
|
||||
stridx = str.lastIndexOf(' ');
|
||||
newstr = str.substr(0, stridx +1) + i;
|
||||
ele.setAttribute('alt', newstr);
|
||||
ele.setAttribute('title', newstr); // For FireFox as 'alt' is not refreshed.
|
||||
ele.setAttribute('title', newstr);
|
||||
// Update the aria-label for the section.
|
||||
sectionlist.item(i).setAttribute('aria-label', content.get('innerText').trim());
|
||||
|
||||
// Remove the current class as section has been moved.
|
||||
sectionlist.item(i).removeClass('current');
|
||||
|
||||
@@ -50,3 +50,4 @@ if (!empty($displaysection)) {
|
||||
}
|
||||
|
||||
$PAGE->requires->js('/course/format/weeks/format.js');
|
||||
$PAGE->requires->js_call_amd('core_course/sectionlistener');
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
.course-content ul.weeks {
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.course-content ul.weeks li.section .content {
|
||||
margin: 0 40px;
|
||||
}
|
||||
|
||||
.course-content ul.weeks li.section .left,
|
||||
.course-content ul.weeks li.section .right {
|
||||
width: 40px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.course-content ul.weeks li.section .left {
|
||||
padding-top: 22px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.jsenabled .course-content ul.weeks li.section .left,
|
||||
.jsenabled .course-content ul.weeks li.section .right {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.course-content ul.weeks li.section .left .section-handle img.icon {
|
||||
padding: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.course-content ul.weeks li.section .section_action_menu .textmenu,
|
||||
.course-content ul.weeks li.section .section_action_menu .menu-action-text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
+4
-1
@@ -1144,7 +1144,9 @@ function course_delete_module($cmid, $async = false) {
|
||||
}
|
||||
|
||||
// Delete activity context questions and question categories.
|
||||
question_delete_activity($cm);
|
||||
$showinfo = !defined('AJAX_SCRIPT') || AJAX_SCRIPT == '0';
|
||||
|
||||
question_delete_activity($cm, $showinfo);
|
||||
|
||||
// Call the delete_instance function, if it returns false throw an exception.
|
||||
if (!$deleteinstancefunction($cm->instance)) {
|
||||
@@ -2202,6 +2204,7 @@ function move_courses($courseids, $categoryid) {
|
||||
foreach ($dbcourses as $dbcourse) {
|
||||
$course = new stdClass();
|
||||
$course->id = $dbcourse->id;
|
||||
$course->timemodified = time();
|
||||
$course->category = $category->id;
|
||||
$course->sortorder = $category->sortorder + MAX_COURSES_IN_CATEGORY - $i++;
|
||||
if ($category->visible == 0) {
|
||||
|
||||
@@ -316,7 +316,8 @@ if ($action !== false && confirm_sesskey()) {
|
||||
$notificationsfail[] = get_string('movecategoryownparent', 'error', $cattomove->get_formatted_name());
|
||||
continue;
|
||||
}
|
||||
if (strpos($movetocat->path, $cattomove->path) === 0) {
|
||||
// Don't allow user to move selected category into one of it's own sub-categories.
|
||||
if (strpos($movetocat->path, $cattomove->path . '/') === 0) {
|
||||
$notificationsfail[] = get_string('movecategoryparentconflict', 'error', $cattomove->get_formatted_name());
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -74,11 +74,8 @@ foreach ($modinfo->cms as $cm) {
|
||||
if (!in_array($cm->modname, $availableresources)) {
|
||||
continue;
|
||||
}
|
||||
if (!$cm->uservisible) {
|
||||
continue;
|
||||
}
|
||||
if (!$cm->has_view()) {
|
||||
// Exclude label and similar
|
||||
// Exclude activities that aren't visible or have no view link (e.g. label). Account for folder being displayed inline.
|
||||
if (!$cm->uservisible || (!$cm->has_view() && strcmp($cm->modname, 'folder') !== 0)) {
|
||||
continue;
|
||||
}
|
||||
$cms[$cm->id] = $cm;
|
||||
@@ -140,9 +137,11 @@ foreach ($cms as $cm) {
|
||||
}
|
||||
|
||||
$class = $cm->visible ? '' : 'class="dimmed"'; // hidden modules are dimmed
|
||||
$url = $cm->url ?: new moodle_url("/mod/{$cm->modname}/view.php", ['id' => $cm->id]);
|
||||
|
||||
$table->data[] = array (
|
||||
$printsection,
|
||||
"<a $class $extra href=\"".$cm->url."\">".$icon.$cm->get_formatted_name()."</a>",
|
||||
"<a $class $extra href=\"" . $url ."\">" . $icon . $cm->get_formatted_name() . "</a>",
|
||||
$intro);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<form action="{{searchurl}}" id="{{id}}" method="get" class="form-inline">
|
||||
<fieldset class="coursesearchbox invisiblefieldset">
|
||||
<label for="{{inputid}}">{{#str}}searchcourses{{/str}}</label>
|
||||
<input id="{{inputid}}" name="search" type="text" size="{{inputsize}}" value="{{value}}" class="form-control">
|
||||
<input id="{{inputid}}" name="search" type="text" size="{{inputsize}}" value="{{value}}" class="form-control mb-1 mb-sm-0">
|
||||
<button class="btn btn-secondary" type="submit">{{#str}}go{{/str}}</button>
|
||||
{{#helpicon}}
|
||||
{{>core/help_icon}}
|
||||
|
||||
@@ -48,6 +48,8 @@ Y.extend(DRAGSECTION, M.core.dragdrop, {
|
||||
this.groups = [CSS.SECTIONDRAGGABLE];
|
||||
this.samenodeclass = M.course.format.get_sectionwrapperclass();
|
||||
this.parentnodeclass = M.course.format.get_containerclass();
|
||||
// Detect the direction of travel.
|
||||
this.detectkeyboarddirection = true;
|
||||
|
||||
// Check if we are in single section mode
|
||||
if (Y.Node.one('.' + CSS.JUMPMENU)) {
|
||||
@@ -147,6 +149,14 @@ Y.extend(DRAGSECTION, M.core.dragdrop, {
|
||||
drag_start: function(e) {
|
||||
// Get our drag object
|
||||
var drag = e.target;
|
||||
// This is the node that the user started to drag.
|
||||
var node = drag.get('node');
|
||||
// This is the container node that will follow the mouse around,
|
||||
// or during a keyboard drag and drop the original node.
|
||||
var dragnode = drag.get('dragNode');
|
||||
if (node === dragnode) {
|
||||
return;
|
||||
}
|
||||
// Creat a dummy structure of the outer elemnents for clean styles application
|
||||
var containernode = Y.Node.create('<' + M.course.format.get_containernode() +
|
||||
'></' + M.course.format.get_containernode() + '>');
|
||||
@@ -155,10 +165,10 @@ Y.extend(DRAGSECTION, M.core.dragdrop, {
|
||||
'></' + M.course.format.get_sectionwrappernode() + '>');
|
||||
sectionnode.addClass(M.course.format.get_sectionwrapperclass());
|
||||
sectionnode.setStyle('margin', 0);
|
||||
sectionnode.setContent(drag.get('node').get('innerHTML'));
|
||||
sectionnode.setContent(node.get('innerHTML'));
|
||||
containernode.appendChild(sectionnode);
|
||||
drag.get('dragNode').setContent(containernode);
|
||||
drag.get('dragNode').addClass(CSS.COURSECONTENT);
|
||||
dragnode.setContent(containernode);
|
||||
dragnode.addClass(CSS.COURSECONTENT);
|
||||
},
|
||||
|
||||
drag_dropmiss: function(e) {
|
||||
@@ -431,6 +441,11 @@ Y.extend(DRAGRESOURCE, M.core.dragdrop, {
|
||||
drag_start: function(e) {
|
||||
// Get our drag object
|
||||
var drag = e.target;
|
||||
if (drag.get('dragNode') === drag.get('node')) {
|
||||
// We do not want to modify the contents of the real node.
|
||||
// They will be the same during a keyboard drag and drop.
|
||||
return;
|
||||
}
|
||||
drag.get('dragNode').setContent(drag.get('node').get('innerHTML'));
|
||||
drag.get('dragNode').all('img.iconsmall').setStyle('vertical-align', 'baseline');
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user