Compare commits
574 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ca15d0f2e8 | |||
| 29dd0f2640 | |||
| dc447e4b3f | |||
| 6e353fbab8 | |||
| c377d42d87 | |||
| d946fae784 | |||
| 77474b866c | |||
| 514fad16ca | |||
| 8a9b201d5c | |||
| 723a63b5a7 | |||
| 3b3bc90d58 | |||
| a5d891fe69 | |||
| 6cc385adb6 | |||
| 619104e303 | |||
| 187bf24f46 | |||
| 6992ef3d3e | |||
| 2a3f719f03 | |||
| 05f233de51 | |||
| e050af3f76 | |||
| 5b36ea0db8 | |||
| 021406c8b8 | |||
| 48031a9e52 | |||
| dc99af1031 | |||
| 84fe6fc634 | |||
| 2f76e2ac11 | |||
| 87cdc2b544 | |||
| 99f773928a | |||
| df803aa0cf | |||
| cac7686d6e | |||
| 7aa7f744cd | |||
| 382b5982a6 | |||
| 00318703e4 | |||
| 5e655ad805 | |||
| 92f5c1cd6c | |||
| 1875710825 | |||
| 5afd98eb7f | |||
| 058c986c13 | |||
| 31a7b4981c | |||
| 4346a59025 | |||
| 71bb810758 | |||
| df843852b9 | |||
| 33ed9d98c7 | |||
| 730534c26c | |||
| 1dcb6c9f56 | |||
| a87b2b8f24 | |||
| 17c2bbf1cb | |||
| c54f62fee6 | |||
| 17e2b5f8cb | |||
| ca06413c3d | |||
| 8fc2c1aab8 | |||
| c741717763 | |||
| 76a4c421d1 | |||
| 9c771234b8 | |||
| bcd1cf6e3d | |||
| b17d59accd | |||
| e88a6c01a4 | |||
| 837e73ce4f | |||
| 58a29073a7 | |||
| 9413a077e9 | |||
| 18c78f4a2e | |||
| 00289a360a | |||
| d11be64b94 | |||
| 314d315397 | |||
| 251c15ee04 | |||
| 21090c3716 | |||
| 8ec1ebc9cd | |||
| 00092de974 | |||
| 9da9ed2bf8 | |||
| a953a3eafb | |||
| f70f1b1721 | |||
| 7a315f18ee | |||
| 4aedbab2e4 | |||
| ca19e78795 | |||
| eb670e1115 | |||
| dbf9adf8a9 | |||
| 407bf7714e | |||
| 5bc19fc1ed | |||
| e9916f9d95 | |||
| e057bc611b | |||
| a0a139f24c | |||
| e24c8e4cde | |||
| 115a369b97 | |||
| a390968760 | |||
| ab645b55d3 | |||
| 1e7b9250dd | |||
| 118f5b53ca | |||
| 2c7984ea33 | |||
| 89689bceeb | |||
| 3ab6ab7d0e | |||
| b00e03b493 | |||
| 04c0d79ccc | |||
| 0a2ce4bd69 | |||
| 381b66b33a | |||
| 90cee9f31c | |||
| 0a19ed5cc1 | |||
| 0d944d9286 | |||
| 65de7c1e0d | |||
| ed263c2eef | |||
| f07ae868a6 | |||
| bc7e7dea82 | |||
| e7fe9f03ff | |||
| 2d3ddd06c1 | |||
| 9868bf12c1 | |||
| 1cd858fb46 | |||
| c16eab825f | |||
| c691cc4e69 | |||
| 7048df7dfb | |||
| cb8592b69f | |||
| 296ae75911 | |||
| ff95fc58b3 | |||
| 42d52d0993 | |||
| 355b839831 | |||
| 2f758dc111 | |||
| 3fbbae99ad | |||
| 8580cb89d8 | |||
| 474ee999f6 | |||
| 4ce0bb6ebf | |||
| f3e8c3a305 | |||
| f0c78f3150 | |||
| ed2c79b77c | |||
| 4e4f7324ea | |||
| 2dee6f2927 | |||
| 2e29b3b6ab | |||
| 431b20d103 | |||
| 120e1da7a9 | |||
| a7a9aa76e6 | |||
| 0f8db4effe | |||
| 5a4fe3dc31 | |||
| f16e99c844 | |||
| b43cd189d5 | |||
| 8d9b0b330f | |||
| b8aa1d15a8 | |||
| 7b19f07fbd | |||
| 4e78b502ef | |||
| 8526d2cd7a | |||
| 2b4a397579 | |||
| 4708188c02 | |||
| 6a6cf5d6ca | |||
| 687559871b | |||
| 53f715d16d | |||
| e26b90c332 | |||
| 6dd0995312 | |||
| 8445cad014 | |||
| 0dc00ed6e9 | |||
| 53a794538a | |||
| 36ded6f300 | |||
| 2be167a172 | |||
| 065b6436c2 | |||
| 1d914c1937 | |||
| c5a878de43 | |||
| 292a559fd7 | |||
| 3569920982 | |||
| 2217677c96 | |||
| 0a75e8e4cf | |||
| 7a6315e676 | |||
| 27b30703b4 | |||
| cfc476a722 | |||
| 093512285f | |||
| 424f20e225 | |||
| 8e2a748167 | |||
| cf470b8de9 | |||
| 8ce2a2fe73 | |||
| 753335e5fe | |||
| cb36a45ce2 | |||
| e8870b1975 | |||
| af12e1f3c6 | |||
| cbc91748b4 | |||
| 3a6e2c8f26 | |||
| d8e559dcbe | |||
| e4c6d0b498 | |||
| cfe8f40e94 | |||
| 4ad38cc139 | |||
| c9978cffbb | |||
| 32b8996a09 | |||
| 1cfd9eec4f | |||
| 4f0592f963 | |||
| c9336ff7dc | |||
| c66e293747 | |||
| 140ee4b80d | |||
| e3ac7a574c | |||
| 1111c96ac0 | |||
| 44d7ffeb89 | |||
| 3fa85e6828 | |||
| 4ea8645494 | |||
| d6b06d0cef | |||
| efee10f43e | |||
| 1b2f009da2 | |||
| 51c1ca0248 | |||
| 41ee4a6cad | |||
| 85e642bec1 | |||
| 0a25a24ff6 | |||
| 5583a50bae | |||
| 82fb415b69 | |||
| b7b20e7381 | |||
| c6575999d6 | |||
| 0effcbe6b5 | |||
| cffd2be100 | |||
| 8270a62377 | |||
| ce5a9b2800 | |||
| 73f101e862 | |||
| 1761d94638 | |||
| d7a8c72b8f | |||
| 1ca9ebb3cb | |||
| af4a6daacc | |||
| 12d7c64dc7 | |||
| 541f1569af | |||
| 8bbc166cb4 | |||
| d26badab50 | |||
| 823d1b25ea | |||
| 662befa64e | |||
| ff135b2d86 | |||
| a7d8f4d1d9 | |||
| 006b2a2642 | |||
| 0c5b52e6ac | |||
| 169827cf80 | |||
| f591916893 | |||
| 6dcb54ced7 | |||
| f105122cef | |||
| 0d4a8ce45d | |||
| 000b0c2771 | |||
| 7afa525ff3 | |||
| a29b5364ab | |||
| fbe62c9538 | |||
| 8523e47075 | |||
| 575932b2ec | |||
| 627945e854 | |||
| 2617f75596 | |||
| 730e1d0ae0 | |||
| 7d02e08ef9 | |||
| 2ca0fb4886 | |||
| 78f545b165 | |||
| 9e4cdf9eca | |||
| be65ecd3e1 | |||
| f597c8bc2d | |||
| 2e541fe5b0 | |||
| 83cc825626 | |||
| cd27ce2970 | |||
| e5bbbe000d | |||
| 7b1bff7cca | |||
| 75de28348a | |||
| 598fee8ec7 | |||
| e968dc4629 | |||
| 170b3ea2ab | |||
| 3d17b48855 | |||
| 0bc7bd66b2 | |||
| 08ea67a272 | |||
| 4d50167046 | |||
| 333aabfaea | |||
| f28de0cb41 | |||
| 7e75a3680e | |||
| d75311461f | |||
| 14467d84fb | |||
| 897649d9a0 | |||
| 3edbc2c282 | |||
| d39da752c6 | |||
| 29381e8a0d | |||
| 1bd8f959e1 | |||
| ebeeb9cef8 | |||
| 9071c67a2c | |||
| 32b8197ad0 | |||
| e7b223c14d | |||
| 027b1a9efe | |||
| 8254bc5d09 | |||
| 630ae866bf | |||
| a3323b7d28 | |||
| 77fed64613 | |||
| ac0ae250f0 | |||
| bb10627261 | |||
| 1dbedf59ae | |||
| 03226fa1be | |||
| 9ade94e0ea | |||
| 9843d8a9ef | |||
| 3d27fbc77d | |||
| 788900f048 | |||
| 274cb0f8b2 | |||
| a54bdf4ade | |||
| 3df91d98f4 | |||
| e54e923b55 | |||
| 0dc8cfc9d4 | |||
| da164d9dba | |||
| fcfd4b3fc3 | |||
| b59cceeeb8 | |||
| d6ed761e7f | |||
| 47cb26cde1 | |||
| a66e710e22 | |||
| ad4887bdf1 | |||
| ab5cb82442 | |||
| cf7a9dd5fc | |||
| c2b06a60bf | |||
| 0d78fef21c | |||
| bd938712f4 | |||
| 27615c8cc6 | |||
| 30d23e5353 | |||
| 24d1f24175 | |||
| f22eed6b67 | |||
| a969e35ef5 | |||
| 43a0e4d96d | |||
| b6cf3a1851 | |||
| dd0b40c2ae | |||
| c9c6dbebd6 | |||
| 13440d2edd | |||
| 866d5c02f3 | |||
| d834f8698f | |||
| 72d54c6b1e | |||
| f113ecfba7 | |||
| dffd193de5 | |||
| a2be2e3067 | |||
| c7d7c5a8c8 | |||
| cb2690ca4e | |||
| af704fe2b3 | |||
| 4332522a7d | |||
| cae8d49d99 | |||
| 320700db21 | |||
| d48c418aa0 | |||
| efd335ca90 | |||
| 588be50d53 | |||
| 52731101f6 | |||
| 3b05390d62 | |||
| a5242728c2 | |||
| ad5b72e696 | |||
| a0ff17c66d | |||
| cf7fe7a547 | |||
| 3101137de3 | |||
| 1c95a3a4da | |||
| 39ce93c3b6 | |||
| e8ae5de91b | |||
| 401a716147 | |||
| d92296b89e | |||
| ede19c68df | |||
| a53ce235a8 | |||
| 30fed30e65 | |||
| 3cc3825f63 | |||
| e8eb81f399 | |||
| 1d21f959b4 | |||
| 67107816a0 | |||
| 8f974b5e0e | |||
| 7cd120d902 | |||
| 2a0c01bb1a | |||
| aaf84bc0cf | |||
| 09daa6585f | |||
| 67dbbc95bd | |||
| f3eedaaf2a | |||
| 5d3fac792c | |||
| 516fed31e5 | |||
| 46cb497bae | |||
| 66c560de3d | |||
| faea71c0aa | |||
| 2e48537ee5 | |||
| d273c20427 | |||
| 376c41e841 | |||
| 59f556e731 | |||
| ccd7a6b9a6 | |||
| 143fbe18f7 | |||
| a444834a5b | |||
| 1b5e5e3c65 | |||
| 1c8cc2b543 | |||
| 659b50f380 | |||
| 77b6cf7568 | |||
| 0b4c755b34 | |||
| 04eb9c0b61 | |||
| 21419b94bd | |||
| 021965fd55 | |||
| 6685d750e7 | |||
| 830fca4a77 | |||
| cdf702bea3 | |||
| 6fb8b72782 | |||
| 2e571156f9 | |||
| 59547c9e26 | |||
| dba907c5a1 | |||
| b09ace0546 | |||
| 360812e77b | |||
| 3f0fa1eb14 | |||
| 1f859c16ea | |||
| a575fe46db | |||
| f0d535b65c | |||
| 0dee8a90e3 | |||
| 0733ed3db1 | |||
| 32547c0496 | |||
| 39111a7bc7 | |||
| a05d3938f8 | |||
| a7f6daff5b | |||
| 90b4e8b85c | |||
| 8c6b8ed7ea | |||
| 1390809635 | |||
| 43a62d768c | |||
| 701e76e301 | |||
| 0b88080611 | |||
| a8f6fad781 | |||
| 6bef9d816c | |||
| c328216c75 | |||
| 566dc85f06 | |||
| a7c7fda91a | |||
| 16b78d03a7 | |||
| 3eb7553ab5 | |||
| 5ef6bc0d7a | |||
| 08abd7ea3b | |||
| 7742681329 | |||
| 3f8d704ee4 | |||
| f53120985a | |||
| ae03be74b6 | |||
| 431605b0ba | |||
| 63e52a69c2 | |||
| 473018cb53 | |||
| e837d357b1 | |||
| f47253d387 | |||
| ba1f355fd5 | |||
| 3a44e5126f | |||
| 1b11ed787b | |||
| 2fda6af72f | |||
| 239b833d99 | |||
| 15f176eb3d | |||
| 7895aca40a | |||
| 9f7839ba0b | |||
| 710db6f11a | |||
| ea1350cbe0 | |||
| 1a9a5d1cbe | |||
| 8553c27e57 | |||
| a18c3ad13a | |||
| 6cc2d8e5db | |||
| 7b17ee7fb9 | |||
| 3feecc5bb8 | |||
| 64dbf8d6c8 | |||
| 7c9fc3fb34 | |||
| 500a225afc | |||
| e8054b547a | |||
| 9ab9d973c7 | |||
| ce2a687c2b | |||
| cbd60e5a9b | |||
| 6f24f29cde | |||
| 3ba2ebb887 | |||
| 501fc68186 | |||
| 42eab03367 | |||
| 9bbcab02b8 | |||
| 0dad8a8369 | |||
| 1387393a3b | |||
| 5919d99222 | |||
| a86c05ee7c | |||
| d58329f1e1 | |||
| 1fcd367669 | |||
| d4f1cb1078 | |||
| ba75747f2e | |||
| 2de34d5c0e | |||
| 85d85a6a25 | |||
| eda944f427 | |||
| 5df88228a5 | |||
| 22b0195c6d | |||
| 5ec7fa94c5 | |||
| f35f246d19 | |||
| 9186e7e0ec | |||
| d00f94ff44 | |||
| 25a13c0394 | |||
| 159976d1ef | |||
| a4c7cb3241 | |||
| 7d00d01b66 | |||
| fa36c75597 | |||
| cf1e71be2b | |||
| 0a50f4c904 | |||
| 683c62f3c6 | |||
| 633ddeb157 | |||
| 3c5accff73 | |||
| d0afa76de6 | |||
| bc2f8102ce | |||
| 532ef50ed6 | |||
| e34037ea6a | |||
| 8f4cb8c4d5 | |||
| 1fc38904e1 | |||
| 4df87b3130 | |||
| 551568aca2 | |||
| 3576ec553c | |||
| 7b8510441f | |||
| e7d7d84aa3 | |||
| ec9b50b135 | |||
| aa4fce5f6a | |||
| 6bf326fb9f | |||
| 482834493e | |||
| a4bd8cec97 | |||
| fa63ee1a8a | |||
| f252b16672 | |||
| 405a99a70b | |||
| 2b5ba79954 | |||
| 1b50220d96 | |||
| ce9d97bd40 | |||
| 2c2aa043c9 | |||
| 5556f401e0 | |||
| 5e2a70cb74 | |||
| 6a2092db12 | |||
| 31f09682df | |||
| 02e0fdb3fe | |||
| 244166e01d | |||
| 5ad933815c | |||
| c111e4fe87 | |||
| 847068d11d | |||
| b577e8d621 | |||
| 98ad4352a9 | |||
| df2723c6a8 | |||
| 201f222789 | |||
| f19403ea2f | |||
| 6c4bf9098e | |||
| 05682343be | |||
| 3d314d6d4b | |||
| afdc24fc07 | |||
| bb3e575079 | |||
| 4f1662e90a | |||
| 9e41cd6609 | |||
| b132ff1ed8 | |||
| 377d56c835 | |||
| 12bf1da9e5 | |||
| 52fbae2907 | |||
| 5eaaf5e016 | |||
| 045db899be | |||
| d42c502516 | |||
| 16c9172c63 | |||
| 6b12fa32e1 | |||
| 9451a354fd | |||
| b9ce55d66d | |||
| cdce19d0d0 | |||
| 168ca6fdef | |||
| f063ef682f | |||
| e06475d8ae | |||
| f0199e8038 | |||
| cca044ce15 | |||
| a780d14710 | |||
| 3af76654fd | |||
| de8176c553 | |||
| c6e0071808 | |||
| 04b0132b3a | |||
| 6e3cb19969 | |||
| 8ca7f282bc | |||
| 33e58ab61a | |||
| 1d40ca9de8 | |||
| c0f24543fc | |||
| fe0c832dcd | |||
| b19296b629 | |||
| c98be745f5 | |||
| 6a34598550 | |||
| ff4f2c02ef | |||
| 9508120293 | |||
| 0891666ec7 | |||
| 17c3e8ecc8 | |||
| c6a31786f7 | |||
| eb0342fa4f | |||
| da47f4dafb | |||
| 8cc95faa8a | |||
| d5ba48ab32 | |||
| d7c719f597 | |||
| 1145b31059 | |||
| 90ebc61746 | |||
| 650efc4ff7 | |||
| 51f425ec29 | |||
| bec6456309 | |||
| 3a9fd31020 | |||
| f7f8b14c3e | |||
| 298a3f5107 | |||
| 70efbd07af | |||
| 5b90cc9653 | |||
| b0423acc58 | |||
| c275ca43c9 | |||
| 746a17f9b0 | |||
| d4f2a8b417 | |||
| eb18a07be2 | |||
| e0930fce1e | |||
| 488a48e5f1 | |||
| ddf3665d9c | |||
| cfef02a97c | |||
| 0f75ce3ebf | |||
| 0df63ca7da | |||
| 1a3e762198 | |||
| 5b8a7b7d73 | |||
| 402ea0f8d2 | |||
| 56a109d2c3 | |||
| 0e9b826517 | |||
| 0d8b41f727 | |||
| ee87a1bcac | |||
| 9567305f70 |
@@ -19,7 +19,7 @@ env:
|
||||
|
||||
jobs:
|
||||
Grunt:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Checking out code
|
||||
@@ -61,12 +61,12 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
# MySQL builds always run with the lowest PHP supported version.
|
||||
- os: ubuntu-22.04
|
||||
- os: ubuntu-24.04
|
||||
php: 8.1
|
||||
extensions:
|
||||
db: mysqli
|
||||
# PostgreSQL builds always run with the highest PHP supported version.
|
||||
- os: ubuntu-22.04
|
||||
- os: ubuntu-24.04
|
||||
php: 8.3
|
||||
db: pgsql
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
issueNumber: MDL-87079
|
||||
notes:
|
||||
core:
|
||||
- message: When responding to pcntl signals, call existing signal handlers.
|
||||
type: fixed
|
||||
@@ -24,6 +24,7 @@
|
||||
*/
|
||||
|
||||
define('CLI_SCRIPT', true);
|
||||
define('IGNORE_COMPONENT_CACHE', true);
|
||||
|
||||
require(__DIR__.'/../../config.php');
|
||||
require_once($CFG->libdir.'/clilib.php');
|
||||
|
||||
@@ -55,13 +55,14 @@ If courseid is set, course module/s will be added into the course.
|
||||
|
||||
Options:
|
||||
-f, --file=STRING Path to the backup file.
|
||||
-c, --categoryid=INT ID of the course category to restore to.
|
||||
-C, --courseid=INT ID of the course to restore to. This option is ignored if categoryid is set.
|
||||
-c, --categoryid=INT ID of the course category to restore to. This option is ignored when restoring an activity and courseid is set.
|
||||
-C, --courseid=INT ID of the course to restore to. This option is ignored when restoring a course and the categoryid is set.
|
||||
-s, --showdebugging Show developer level debugging information
|
||||
-h, --help Print out this help.
|
||||
|
||||
Example:
|
||||
\$sudo -u www-data /usr/bin/php admin/cli/restore_backup.php --file=/path/to/backup/file.mbz --categoryid=1\n
|
||||
\$sudo -u www-data /usr/bin/php admin/cli/restore_backup.php --file=/path/to/backup/coursebackup.mbz --categoryid=1\n
|
||||
\$sudo -u www-data /usr/bin/php admin/cli/restore_backup.php --file=/path/to/backup/activitybackup.mbz --courseid=1\n
|
||||
EOL;
|
||||
|
||||
echo $help;
|
||||
@@ -84,11 +85,15 @@ if ($options['categoryid']) {
|
||||
if (!$category = $DB->get_record('course_categories', ['id' => $options['categoryid']], 'id')) {
|
||||
throw new \moodle_exception('invalidcategoryid');
|
||||
}
|
||||
} else if ($options['courseid']) {
|
||||
}
|
||||
|
||||
if ($options['courseid']) {
|
||||
if (!$course = $DB->get_record('course', ['id' => $options['courseid']], 'id')) {
|
||||
throw new \moodle_exception('invalidcourseid');
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
if (empty($category) && empty($course)) {
|
||||
throw new \moodle_exception('invalidoption');
|
||||
}
|
||||
|
||||
@@ -100,28 +105,46 @@ $fp = get_file_packer('application/vnd.moodle.backup');
|
||||
$fp->extract_to_pathname($options['file'], $path);
|
||||
|
||||
cli_heading(get_string('preprocessingbackupfile'));
|
||||
try {
|
||||
list($fullname, $shortname) = restore_dbops::calculate_course_names(0, get_string('restoringcourse', 'backup'),
|
||||
get_string('restoringcourseshortname', 'backup'));
|
||||
|
||||
if (!empty($course)) {
|
||||
$courseid = $course->id;
|
||||
$rc = new restore_controller($backupdir, $courseid, backup::INTERACTIVE_NO,
|
||||
backup::MODE_GENERAL, $admin->id, backup::TARGET_EXISTING_ADDING);
|
||||
} else {
|
||||
try {
|
||||
// Create a temporary restore controller to determine the restore type.
|
||||
$tmprc = new restore_controller($backupdir, SITEID, backup::INTERACTIVE_NO,
|
||||
backup::MODE_GENERAL, $admin->id, backup::TARGET_EXISTING_ADDING);
|
||||
// Restore the backup into a new course if:
|
||||
// - It is a course backup and the category is set.
|
||||
// - It is an activity backup and the course is not set.
|
||||
$restoreasnewcourse = ($tmprc->get_type() === backup::TYPE_1COURSE && !empty($category)) ||
|
||||
($tmprc->get_type() !== backup::TYPE_1COURSE && empty($course));
|
||||
// Make sure to clean up the temporary restore controller.
|
||||
$tmprc->destroy();
|
||||
|
||||
if ($restoreasnewcourse) {
|
||||
list($fullname, $shortname) = restore_dbops::calculate_course_names(0, get_string('restoringcourse', 'backup'),
|
||||
get_string('restoringcourseshortname', 'backup'));
|
||||
$courseid = restore_dbops::create_new_course($fullname, $shortname, $category->id);
|
||||
$rc = new restore_controller($backupdir, $courseid, backup::INTERACTIVE_NO,
|
||||
backup::MODE_GENERAL, $admin->id, backup::TARGET_NEW_COURSE);
|
||||
} else {
|
||||
$courseid = $course->id;
|
||||
$rc = new restore_controller($backupdir, $courseid, backup::INTERACTIVE_NO,
|
||||
backup::MODE_GENERAL, $admin->id, backup::TARGET_EXISTING_ADDING);
|
||||
}
|
||||
$rc->execute_precheck();
|
||||
$rc->execute_plan();
|
||||
$rc->destroy();
|
||||
|
||||
// Rename course name if the backup is from course module and restore to category.
|
||||
if (empty($course)) {
|
||||
// Rename the course's full and short names with the backup file's original names if the backup file is an activity backup
|
||||
// that is restored to a new course.
|
||||
if ($restoreasnewcourse && $rc->get_type() !== backup::TYPE_1COURSE) {
|
||||
$course = get_course($courseid);
|
||||
list($fullname, $shortname) = restore_dbops::calculate_course_names(0, get_string('restoretonewcourse', 'backup'),
|
||||
get_string('newcourse'));
|
||||
$backupinfo = $rc->get_info();
|
||||
$tmpfullname = $backupinfo->original_course_fullname ?? get_string('restoretonewcourse', 'backup');
|
||||
$tmpshortname = $backupinfo->original_course_shortname ?? get_string('newcourse');
|
||||
list($fullname, $shortname) = restore_dbops::calculate_course_names(
|
||||
courseid: 0,
|
||||
fullname: $tmpfullname,
|
||||
shortname: $tmpshortname,
|
||||
);
|
||||
$course->fullname = $fullname;
|
||||
$course->shortname = $shortname;
|
||||
$course->visible = 1;
|
||||
|
||||
@@ -3835,6 +3835,7 @@
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="optional"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
<PHP_EXTENSION name="filter" level="required"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
@@ -4327,6 +4328,7 @@
|
||||
<VENDOR name="oracle" version="19" />
|
||||
</DATABASE>
|
||||
<PHP version="8.1.0" level="required">
|
||||
<RESTRICT function="restrict_php_version_84" message="unsupportedphpversion84" />
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
@@ -4410,6 +4412,7 @@
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="required"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
<PHP_EXTENSION name="filter" level="required"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
@@ -4522,6 +4525,7 @@
|
||||
<VENDOR name="oracle" version="19" />
|
||||
</DATABASE>
|
||||
<PHP version="8.1.0" level="required">
|
||||
<RESTRICT function="restrict_php_version_84" message="unsupportedphpversion84" />
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
@@ -4605,6 +4609,7 @@
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="required"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
<PHP_EXTENSION name="filter" level="required"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
@@ -4714,6 +4719,7 @@
|
||||
<VENDOR name="mysql" version="8.4" />
|
||||
<VENDOR name="postgres" version="14" />
|
||||
<VENDOR name="mssql" version="14.0" />
|
||||
<VENDOR name="auroramysql" version="8.0" />
|
||||
</DATABASE>
|
||||
<PHP version="8.2.0" level="required">
|
||||
</PHP>
|
||||
@@ -4799,6 +4805,7 @@
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="required"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
<PHP_EXTENSION name="filter" level="required"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
@@ -4893,6 +4900,413 @@
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_async_backup" level="recommended">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_aurora_version" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="5.1" requires="4.2.3">
|
||||
<UNICODE level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unicoderequired" />
|
||||
</FEEDBACK>
|
||||
</UNICODE>
|
||||
<DATABASE level="required">
|
||||
<VENDOR name="mariadb" version="10.11.0" />
|
||||
<VENDOR name="mysql" version="8.4" />
|
||||
<VENDOR name="postgres" version="15" />
|
||||
<VENDOR name="mssql" version="14.0" />
|
||||
<VENDOR name="auroramysql" version="8.0" />
|
||||
</DATABASE>
|
||||
<PHP version="8.2.0" level="required">
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="pcreunicodewarning" />
|
||||
</FEEDBACK>
|
||||
</PCREUNICODE>
|
||||
<PHP_EXTENSIONS>
|
||||
<PHP_EXTENSION name="iconv" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="iconvrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="mbstring" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="mbstringrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="curl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="curlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="openssl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="opensslrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="tokenizer" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="tokenizerrecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="soap" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="soaprecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="ctype" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="ctyperequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="zip" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="ziprequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="zlib" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="gd" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="gdrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="simplexml" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="simplexmlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="spl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="splrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="pcre" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="dom" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xml" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xmlreader" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="intl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="intlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="json" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="hash" level="required"/>
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="required"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
<PHP_EXTENSION name="filter" level="required"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="settingmemorylimit" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="file_uploads" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="settingfileuploads" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="opcache.enable" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="opcacherecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="zend.exception_ignore_args" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="settingzendexceptionignoreargs" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
</PHP_SETTINGS>
|
||||
<CUSTOM_CHECKS>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbstorageengine" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="quizattemptsupgradedmessage" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="slashargumentswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="unsupporteddbtablerowformat" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="unoconvwarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="libcurlwarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbfileformat" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbfilepertable" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddblargeprefix" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="ishttpswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="incompleteunicodesupport" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_sixtyfour_bits" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="sixtyfourbitsrequired" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_max_input_vars" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_admin_dir_usage" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="iscustomadminwarnings" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_xmlrpc_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mod_assignment" level="required">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_db_prefix_length" level="required">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_async_backup" level="recommended">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_aurora_version" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="5.2" requires="4.4">
|
||||
<UNICODE level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unicoderequired" />
|
||||
</FEEDBACK>
|
||||
</UNICODE>
|
||||
<DATABASE level="required">
|
||||
<VENDOR name="mariadb" version="10.11.0" />
|
||||
<VENDOR name="mysql" version="8.4" />
|
||||
<VENDOR name="postgres" version="16" />
|
||||
<VENDOR name="mssql" version="15.0" />
|
||||
<VENDOR name="auroramysql" version="8.0" />
|
||||
</DATABASE>
|
||||
<PHP version="8.3.0" level="required">
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="pcreunicodewarning" />
|
||||
</FEEDBACK>
|
||||
</PCREUNICODE>
|
||||
<PHP_EXTENSIONS>
|
||||
<PHP_EXTENSION name="iconv" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="iconvrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="mbstring" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="mbstringrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="curl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="curlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="openssl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="opensslrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="tokenizer" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="tokenizerrecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="soap" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="soaprecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="ctype" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="ctyperequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="zip" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="ziprequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="zlib" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="gd" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="gdrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="simplexml" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="simplexmlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="spl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="splrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="pcre" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="dom" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xml" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xmlreader" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="intl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="intlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="json" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="hash" level="required"/>
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="required"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
<PHP_EXTENSION name="filter" level="required"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="settingmemorylimit" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="file_uploads" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="settingfileuploads" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="opcache.enable" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="opcacherecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="zend.exception_ignore_args" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="settingzendexceptionignoreargs" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
</PHP_SETTINGS>
|
||||
<CUSTOM_CHECKS>
|
||||
<CUSTOM_CHECK function="\core\environment::check_composer_dependencies_installed" level="optional" />
|
||||
<CUSTOM_CHECK function="\core\environment::check_composer_developer_dependencies_not_installed" level="optional" />
|
||||
<CUSTOM_CHECK function="\core\environment::check_composer_dependencies_optimised" level="optional" />
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbstorageengine" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="quizattemptsupgradedmessage" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="slashargumentswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="unsupporteddbtablerowformat" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="unoconvwarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="libcurlwarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbfileformat" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbfilepertable" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddblargeprefix" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="ishttpswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="incompleteunicodesupport" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_sixtyfour_bits" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="sixtyfourbitsrequired" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_max_input_vars" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_admin_dir_usage" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="iscustomadminwarnings" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_xmlrpc_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mod_assignment" level="required">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_db_prefix_length" level="required">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_async_backup" level="recommended">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_aurora_version" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
</COMPATIBILITY_MATRIX>
|
||||
|
||||
+8
-2
@@ -923,8 +923,14 @@ if (empty($CFG->disabledevlibdirscheck) && (is_dir($CFG->dirroot.'/vendor') || i
|
||||
// Check if the site is being foced onto ssl.
|
||||
$overridetossl = !empty($CFG->overridetossl);
|
||||
|
||||
// Check if moodle campaign content setting is enabled or not.
|
||||
$showcampaigncontent = !isset($CFG->showcampaigncontent) || $CFG->showcampaigncontent;
|
||||
if (defined('BEHAT_SITE_RUNNING') && BEHAT_SITE_RUNNING) {
|
||||
// We need to add this in order to be able to verify the showcampaigncontent setting behaviour during behat tests.
|
||||
$showcampaigncontent = get_config('core', 'showcampaigncontent');
|
||||
$showcampaigncontent = $showcampaigncontent === 'true';
|
||||
} else {
|
||||
// Check if moodle campaign content setting is enabled or not.
|
||||
$showcampaigncontent = !isset($CFG->showcampaigncontent) || $CFG->showcampaigncontent;
|
||||
}
|
||||
|
||||
// Encourage admins to enable the user feedback feature if it is not enabled already.
|
||||
$showfeedbackencouragement = empty($CFG->enableuserfeedback);
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
define('IGNORE_COMPONENT_CACHE', true);
|
||||
|
||||
require_once('../config.php');
|
||||
require_once($CFG->libdir.'/adminlib.php');
|
||||
|
||||
|
||||
+2
-1
@@ -2180,10 +2180,11 @@ class core_admin_renderer extends plugin_renderer_base {
|
||||
|
||||
$output = '';
|
||||
$output .= $this->header();
|
||||
$output .= $this->container_start('upgradekeyreq');
|
||||
$output .= $this->heading(get_string('upgradekeyreq', 'core_admin'));
|
||||
$output .= $this->container_start('upgradekeyreq w-25');
|
||||
$output .= html_writer::start_tag('form', array('method' => 'POST', 'action' => $url));
|
||||
$output .= html_writer::empty_tag('input', [
|
||||
'id' => 'upgradekey',
|
||||
'name' => 'upgradekey',
|
||||
'type' => 'password',
|
||||
'class' => 'form-control w-auto',
|
||||
|
||||
@@ -51,6 +51,14 @@ if ($hassiteconfig) {
|
||||
|
||||
$temp->add(new admin_setting_countrycodes('allcountrycodes', new lang_string('allcountrycodes', 'core_admin'),
|
||||
new lang_string('configallcountrycodes', 'core_admin')));
|
||||
|
||||
$temp->add(new admin_setting_configtext(
|
||||
'geopluginapikey',
|
||||
new lang_string('geopluginapikey', 'core_admin'),
|
||||
new lang_string('geopluginapikey_desc', 'core_admin'),
|
||||
'',
|
||||
PARAM_TEXT,
|
||||
));
|
||||
}
|
||||
|
||||
$ADMIN->add('location', $temp);
|
||||
|
||||
@@ -81,6 +81,8 @@ class behat_admin extends behat_base {
|
||||
|
||||
$this->execute('behat_forms::i_set_the_field_with_xpath_to', [$fieldxpath, $value]);
|
||||
$this->execute("behat_general::i_click_on", [get_string('savechanges'), 'button']);
|
||||
// Wait for the page to be redirected.
|
||||
$this->execute("behat_general::i_wait_to_be_redirected");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ Feature: An administrator can browse user accounts
|
||||
| username | firstname | lastname | email | confirmed |
|
||||
| user3 | User | Three | three@example.com | 0 |
|
||||
And I navigate to "Users > Accounts > Browse list of users" in site administration
|
||||
And I change window size to "large"
|
||||
Then I should see "Confirmation pending" in the "User Three" "table_row"
|
||||
And I press "Resend confirmation email" action in the "User Three" report row
|
||||
And I should see "Confirmation email sent successfully"
|
||||
|
||||
@@ -18,6 +18,7 @@ Feature: Verify the breadcrumbs in webservice tokens site administration pages
|
||||
And "Manage tokens" "link" should exist in the ".breadcrumb" "css_element"
|
||||
And "Web services" "link" should exist in the ".breadcrumb" "css_element"
|
||||
And I set the field "User" to "John Doe"
|
||||
And I set the field "Service" to "Moodle mobile web service"
|
||||
And I press "Save changes"
|
||||
When I click on "Delete" "link"
|
||||
Then "Delete token" "text" should exist in the ".breadcrumb" "css_element"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
@core @core_admin
|
||||
Feature: Toggle campaign banner visibility
|
||||
In order to control the visibility of the campaign banner content
|
||||
As an admin
|
||||
I need to be able to disable the campaign banner display
|
||||
|
||||
Scenario Outline: Admin can disable the campaign banner display
|
||||
Given the following config values are set as admin:
|
||||
| showcampaigncontent | <showcampaigncontent> |
|
||||
And I log in as "admin"
|
||||
When I navigate to "Notifications" in site administration
|
||||
Then "//iframe[@id='campaign-content']" "xpath_element" <display> exist
|
||||
|
||||
Examples:
|
||||
| showcampaigncontent | display |
|
||||
| true | should |
|
||||
| false | should not |
|
||||
@@ -0,0 +1,79 @@
|
||||
@tool @tool_analytics
|
||||
Feature: Manager can obtain prediction models insights
|
||||
In order to view prediction models insights
|
||||
As a manager
|
||||
I should be able to obtain the prediction models insights
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| s1 | Student | One | s1@example.com |
|
||||
| m1 | Manager | One | m1@example.com |
|
||||
| m2 | Manager | Two | m2@example.com |
|
||||
| t1 | Teacher | One | t1@example.com |
|
||||
And the following "role assigns" exist:
|
||||
| user | role | contextlevel | reference |
|
||||
| m1 | manager | System | |
|
||||
| m2 | manager | System | |
|
||||
# To ensure that course start date is in the future, set course start date to "tomorrow".
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | startdate |
|
||||
| Course 1 | C1 | ##tomorrow## |
|
||||
| Course 2 | C2 | ##tomorrow## |
|
||||
| Course 3 | C3 | ##tomorrow## |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| s1 | C1 | student |
|
||||
| s1 | C2 | student |
|
||||
| s1 | C3 | student |
|
||||
| t1 | C3 | editingteacher |
|
||||
# Disable "Analytics processes execution via command line only".
|
||||
And the following config values are set as admin:
|
||||
| onlycli | 0 | analytics |
|
||||
|
||||
@javascript
|
||||
Scenario: Manager can obtain prediction models insights
|
||||
Given I log in as "m1"
|
||||
And I navigate to "Analytics > Analytics models" in site administration
|
||||
And I click on "Actions" "link" in the "Courses at risk of not starting" "table_row"
|
||||
And I click on "Execute scheduled analysis" "link" in the "Courses at risk of not starting" "table_row"
|
||||
And I press "Continue"
|
||||
When I select "All predictions" from the "contextid" singleselect
|
||||
# Confirm that only courses without teachers, Course 1 and Course 2, are listed.
|
||||
Then "Course 1" "text" should exist
|
||||
And "Course 2" "text" should exist
|
||||
And "Course 3" "text" should not exist
|
||||
And I click on "Select Course 1 for bulk action" "checkbox"
|
||||
And I press "Not applicable"
|
||||
And I press "Confirm"
|
||||
# After m1 marks Course 1 as Not applicable, only Course 2 should be listed in the predictions for manager 1.
|
||||
And "Course 1" "text" should not exist
|
||||
And "Course 2" "text" should exist
|
||||
And I log in as "m2"
|
||||
And I click on ".popover-region-notifications" "css_element"
|
||||
# Notification of new insight exists.
|
||||
And "Upcoming courses have no teachers or students" "text" should exist
|
||||
And I click on "View full notification" "link" in the ".popover-region-notifications" "css_element"
|
||||
And I click on "View insight" "link"
|
||||
# Course 1, previously marked as Not applicable by manager 1, is still listed for manager 2.
|
||||
And "Course 1" "text" should exist
|
||||
And I click on "View details" "link" in the "Course 1" "table_row"
|
||||
# Prediction details and indicators are displayed for the selected course.
|
||||
And the following should exist in the "insights-list" table:
|
||||
| Description |
|
||||
| Course 1 |
|
||||
And the following should exist in the "prediction-calculations" table:
|
||||
| -1- | -2- |
|
||||
| Teacher availability | No |
|
||||
| Student enrolments | Yes |
|
||||
# Date of prediction analysis execution.
|
||||
And "##today##%A, %d %B %Y##" "text" should exist in the "Time predicted" "table_row"
|
||||
And I click on "Select Course 1 for bulk action" "checkbox"
|
||||
And I press "Accept"
|
||||
And I press "Confirm"
|
||||
# Verify that you end up on the page listing predictions for 'No teaching'.
|
||||
And "Courses at risk of not starting" "text" should exist
|
||||
And "The following courses due to start in the upcoming days are at risk of not starting because they don't have teachers or students enrolled." "text" should exist
|
||||
And the following should not exist in the "insights-list" table:
|
||||
| Description |
|
||||
| Course 1 |
|
||||
@@ -36,6 +36,10 @@ if (function_exists('opcache_reset')) {
|
||||
define('CLI_SCRIPT', true);
|
||||
define('CACHE_DISABLE_ALL', true);
|
||||
|
||||
// It makes no sense to use BEHAT_CLI for this script (the Behat launch scripts expect to start
|
||||
// from the normal environment), so in case user has set tne environment variable, disable it.
|
||||
putenv('BEHAT_CLI=0');
|
||||
|
||||
// Basic functions.
|
||||
require_once(__DIR__ . '/../../../../lib/clilib.php');
|
||||
require_once(__DIR__ . '/../../../../lib/behat/lib.php');
|
||||
|
||||
@@ -31,6 +31,10 @@ define('ABORT_AFTER_CONFIG', true);
|
||||
define('CACHE_DISABLE_ALL', true);
|
||||
define('NO_OUTPUT_BUFFERING', true);
|
||||
|
||||
// It makes no sense to use BEHAT_CLI for this script (the Behat launch scripts expect to start
|
||||
// from the normal environment), so in case user has set tne environment variable, disable it.
|
||||
putenv('BEHAT_CLI=0');
|
||||
|
||||
require_once(__DIR__ .'/../../../../config.php');
|
||||
require_once(__DIR__.'/../../../../lib/clilib.php');
|
||||
require_once(__DIR__.'/../../../../lib/behat/lib.php');
|
||||
|
||||
@@ -39,6 +39,10 @@ define('NO_OUTPUT_BUFFERING', true);
|
||||
define('IGNORE_COMPONENT_CACHE', true);
|
||||
define('ABORT_AFTER_CONFIG', true);
|
||||
|
||||
// It makes no sense to use BEHAT_CLI for this script (the Behat launch scripts expect to start
|
||||
// from the normal environment), so in case user has set tne environment variable, disable it.
|
||||
putenv('BEHAT_CLI=0');
|
||||
|
||||
require_once(__DIR__ . '/../../../../lib/clilib.php');
|
||||
|
||||
// CLI options.
|
||||
|
||||
@@ -33,6 +33,10 @@ if (isset($_SERVER['REMOTE_ADDR'])) {
|
||||
die(); // No access from web!.
|
||||
}
|
||||
|
||||
// It makes no sense to use BEHAT_CLI for this script (the Behat launch scripts expect to start
|
||||
// from the normal environment), so in case user has set tne environment variable, disable it.
|
||||
putenv('BEHAT_CLI=0');
|
||||
|
||||
// Basic functions.
|
||||
require_once(__DIR__ . '/../../../../lib/clilib.php');
|
||||
require_once(__DIR__ . '/../../../../lib/behat/lib.php');
|
||||
|
||||
+1
-5
@@ -30,10 +30,6 @@ use tool_brickfield\local\htmlchecker\common\brickfield_accessibility_test;
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class img_alt_is_too_long extends brickfield_accessibility_test {
|
||||
|
||||
/** @var int The default severity code for this test. */
|
||||
public $defaultseverity = \tool_brickfield\local\htmlchecker\brickfield_accessibility::BA_TEST_SEVERE;
|
||||
|
||||
/**
|
||||
* The main check function. This is called by the parent class to actually check content.
|
||||
*/
|
||||
@@ -41,7 +37,7 @@ class img_alt_is_too_long extends brickfield_accessibility_test {
|
||||
global $alttextlengthlimit;
|
||||
|
||||
foreach ($this->get_all_elements('img') as $img) {
|
||||
$alttextlengthlimit = 125;
|
||||
$alttextlengthlimit = 750;
|
||||
if ($img->hasAttribute('alt') && core_text::strlen($img->getAttribute('alt')) > $alttextlengthlimit) {
|
||||
$this->add_report($img);
|
||||
}
|
||||
|
||||
@@ -402,6 +402,8 @@ class manager {
|
||||
}
|
||||
}
|
||||
|
||||
$rs->close();
|
||||
|
||||
if (count($batchinserts) > 0) {
|
||||
$DB->insert_records(self::DB_PROCESS, $batchinserts);
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ $action = optional_param('action', '', PARAM_ALPHA);
|
||||
// Handle any single operation actions.
|
||||
if ($action == 'requestanalysis') {
|
||||
if ($courseid != 0) {
|
||||
require_sesskey();
|
||||
scheduler::request_course_analysis($courseid);
|
||||
if ($courseid == SITEID) {
|
||||
redirect(accessibility::get_plugin_url());
|
||||
|
||||
@@ -179,7 +179,7 @@ $string['checkdesc:headerh3'] = 'Headers following after H3 headers (the editor
|
||||
$string['checkdesc:headershavetext'] = 'A header needs to contain text to be perceivable.';
|
||||
$string['checkdesc:iisnotused'] = 'Italic (i) elements should not be used; "em" should be used instead.';
|
||||
$string['checkdesc:imgaltisdifferent'] = 'Image alt (alternative) text should not be the image filename.';
|
||||
$string['checkdesc:imgaltistoolong'] = 'Image alt (alternative) text should not be more than the maximum allowed (125) characters.';
|
||||
$string['checkdesc:imgaltistoolong'] = 'Ensure that the image alt (alternative) text is concise enough to describe the image.';
|
||||
$string['checkdesc:imgaltnotemptyinanchor'] = 'Image alt (alternative) text should not be empty, especially when the image has a link going elsewhere.';
|
||||
$string['checkdesc:imgaltnotplaceholder'] = 'Image alt (alternative) text should not be a simple placeholder text, such as "image".';
|
||||
$string['checkdesc:imghasalt'] = 'Image alt (alternative) text should not be missing for image elements, unless purely decorative with no meaning.';
|
||||
|
||||
+22
-9
@@ -24,6 +24,8 @@
|
||||
|
||||
namespace tool_brickfield\local\htmlchecker\common\checks;
|
||||
|
||||
use tool_brickfield\local\htmlchecker\brickfield_accessibility;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once('all_checks.php');
|
||||
@@ -64,21 +66,21 @@ EOD;
|
||||
*/
|
||||
public static function img_alt_text_provider(): array {
|
||||
return [
|
||||
'Alt text <= 125 characters' => [
|
||||
'Alt text <= 750 characters' => [
|
||||
true,
|
||||
str_repeat("Hello world!", 10),
|
||||
str_repeat("Hello world!", 60),
|
||||
],
|
||||
'Alt text > 125 characters' => [
|
||||
'Alt text > 750 characters' => [
|
||||
false,
|
||||
str_repeat("Hello world!", 25),
|
||||
str_repeat("Hello world!", 65),
|
||||
],
|
||||
'Multi-byte alt text <= 125 characters' => [
|
||||
'Multi-byte alt text <= 750 characters' => [
|
||||
true,
|
||||
str_repeat('こんにちは、世界!', 13),
|
||||
str_repeat('こんにちは、世界!', 83),
|
||||
],
|
||||
'Multi-byte alt text > 125 characters' => [
|
||||
'Multi-byte alt text > 750 characters' => [
|
||||
false,
|
||||
str_repeat('こんにちは、世界!', 30),
|
||||
str_repeat('こんにちは、世界!', 90),
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -96,7 +98,18 @@ EOD;
|
||||
if ($expectedpass) {
|
||||
$this->assertEmpty($results);
|
||||
} else {
|
||||
$this->assertTrue($results[0]->element->tagName === 'img');
|
||||
$this->assertEquals('img', $results[0]->element->tagName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the severity of the {@see img_alt_is_too_long} check.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_severity(): void {
|
||||
$html = $this->get_test_html('Some alt text');
|
||||
$checker = new brickfield_accessibility($html, 'brickfield', 'string');
|
||||
$this->assertEquals(brickfield_accessibility::BA_TEST_SUGGESTION, $checker->get_test_severity(img_alt_is_too_long::class));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
namespace tool_capability;
|
||||
|
||||
// phpcs:disable moodle.PHPUnit.TestCaseProvider.dataProviderSyntaxMethodNotFound
|
||||
|
||||
/**
|
||||
* Detect common problems in capability definitions of plugins.
|
||||
*
|
||||
* @group plugin_checks
|
||||
* @package tool_capability
|
||||
* @copyright 2025 Petr Skoda
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
final class plugin_checks_test extends \core\tests\plugin_checks_testcase {
|
||||
/**
|
||||
* Verify contents of plugin db/access.php file.
|
||||
*
|
||||
* @dataProvider all_plugins_provider
|
||||
* @coversNothing
|
||||
*
|
||||
* @param string $component
|
||||
* @param string $plugintype
|
||||
* @param string $pluginname
|
||||
* @param string $dir
|
||||
*/
|
||||
public function test_db_access_file(string $component, string $plugintype, string $pluginname, string $dir): void {
|
||||
global $CFG;
|
||||
|
||||
$stringmanager = get_string_manager();
|
||||
$corerolefile = "$CFG->dirroot/lang/en/role.php";
|
||||
$langfile = "$dir/lang/en/$component.php";
|
||||
|
||||
$file = "$dir/db/access.php";
|
||||
$capabilities = $this->fetch_array_from_file($file, 'capabilities');
|
||||
if (!$capabilities) {
|
||||
$this->expectNotToPerformAssertions();
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($capabilities as $capname => $capability) {
|
||||
if ($plugintype === 'qbank' && str_starts_with($capname, 'moodle/question:')) {
|
||||
// Question bank capabilities are irregular.
|
||||
$strname = explode('/', $capname, 2)[1];
|
||||
$this->assertTrue($stringmanager->string_exists($strname, 'core_role'),
|
||||
"Missing capability name string '$strname' in $corerolefile");
|
||||
continue;
|
||||
}
|
||||
$this->assertMatchesRegularExpression("|^$plugintype/$pluginname:[a-z0-9_]+$|", $capname);
|
||||
$strname = substr($capname, strlen($plugintype) + 1);
|
||||
$this->assertTrue($stringmanager->string_exists($strname, $component),
|
||||
"Missing capability name string '$strname' in $langfile");
|
||||
$this->assertSame($capname, clean_param($capname, PARAM_CAPABILITY));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,7 +175,7 @@ class cohort_role_assignments_table extends table_sql {
|
||||
* @return array containing sql to use and an array of params.
|
||||
*/
|
||||
protected function get_sql_and_params($count = false) {
|
||||
$fields = 'uca.id, uca.cohortid, uca.userid, uca.roleid, ';
|
||||
$fields = 'uca.id, uca.cohortid, uca.userid, uca.roleid, r.shortname AS rolename, ';
|
||||
$fields .= 'c.name as cohortname, c.idnumber as cohortidnumber, c.contextid as cohortcontextid, ';
|
||||
$fields .= 'c.visible as cohortvisible, c.description as cohortdescription, c.theme as cohorttheme';
|
||||
|
||||
@@ -193,6 +193,7 @@ class cohort_role_assignments_table extends table_sql {
|
||||
$sql = "SELECT $select
|
||||
FROM {tool_cohortroles} uca
|
||||
JOIN {user} u ON u.id = uca.userid
|
||||
JOIN {role} r ON r.id = uca.roleid
|
||||
JOIN {cohort} c ON c.id = uca.cohortid";
|
||||
|
||||
// Check if any additional filtering is required.
|
||||
|
||||
@@ -123,6 +123,7 @@ class expired_contexts_manager {
|
||||
$expiredcontext = new expired_context(0, $orphan);
|
||||
$expiredcontext->delete();
|
||||
}
|
||||
$orphaned->close();
|
||||
|
||||
// Delete any child of a user context.
|
||||
$parentpath = $DB->sql_concat('ctxuser.path', "'/%'");
|
||||
@@ -143,6 +144,7 @@ class expired_contexts_manager {
|
||||
$expiredcontext = new expired_context(0, $child);
|
||||
$expiredcontext->delete();
|
||||
}
|
||||
$userchildren->close();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -127,7 +127,6 @@ $string['dporolemapping'] = 'Privacy officer role mapping';
|
||||
$string['dporolemapping_desc'] = 'The privacy officer can manage data requests. The capability tool/dataprivacy:managedatarequests must be allowed for a role to be listed as a privacy officer role mapping option.';
|
||||
$string['editcategories'] = 'Edit categories';
|
||||
$string['editcategory'] = 'Edit category';
|
||||
$string['editcategories'] = 'Edit categories';
|
||||
$string['editdefaults'] = 'Edit defaults: {$a}';
|
||||
$string['editmoduledefaults'] = 'Edit module defaults';
|
||||
$string['editpurpose'] = 'Edit purpose';
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
@tool @tool_lp @tool_lp_framework
|
||||
Feature: Manage CRUD operations for competencies
|
||||
In order to perform CRUD operations on competencies
|
||||
As a manager
|
||||
I need to be able to create, read, update and delete competencies
|
||||
|
||||
Background:
|
||||
Given the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| CF1 | CF1 |
|
||||
And the following "core_competency > competencies" exist:
|
||||
| shortname | competencyframework | idnumber | description |
|
||||
| C1 | CF1 | C1ID | C1 description |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Competencies > Competency frameworks" in site administration
|
||||
And I click on "CF1 (CF1)" "link"
|
||||
|
||||
@javascript
|
||||
Scenario: Create a new competency
|
||||
# Targets the `CF1` element to make `Add competency` button visible and accessible to avoid ambiguity with the other 'CF1' element on the screen.
|
||||
Given I click on "//span[text()='CF1']" "xpath_element"
|
||||
When I press "Add competency"
|
||||
And I set the field "Name" to "C2"
|
||||
And I set the field "Description" to "C2 description"
|
||||
And I set the field "ID number" to "C2ID"
|
||||
And I press "Save changes"
|
||||
# Access the newly created competencies to ensure that correct information was registered.
|
||||
Then "Competency created" "text" should exist
|
||||
And "C2" "text" should appear after "C1" "text"
|
||||
And I select "C2" of the competency tree
|
||||
And "C2ID" "text" should exist
|
||||
And "C2 description" "text" should exist
|
||||
|
||||
@javascript
|
||||
Scenario: Read a competency
|
||||
When I select "C1" of the competency tree
|
||||
# Confirm that selected competency info displayed matches registered info.
|
||||
Then "C1ID" "text" should exist
|
||||
And "C1 description" "text" should exist
|
||||
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
|
||||
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
|
||||
# Similar to the previous step, to avoid ambiguity with the competency framework "Edit", target css element with data-action=edit.
|
||||
And I click on "[data-action=edit]" "css_element"
|
||||
# Confirm that the details displayed when accessing edit screen values match registered data.
|
||||
And the field "Name" matches value "C1"
|
||||
And the field "Description" matches value "C1 description"
|
||||
And the field "ID number" matches value "C1ID"
|
||||
|
||||
@javascript
|
||||
Scenario: Update a competency
|
||||
Given I select "C1" of the competency tree
|
||||
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
|
||||
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
|
||||
# Similar to the previous step, to avoid ambiguity with the competency framework "Edit", target css element with data-action=edit.
|
||||
When I click on "[data-action=edit]" "css_element"
|
||||
And I set the field "Name" to "C2"
|
||||
And I set the field "Description" to "C2 description"
|
||||
And I set the field "ID number" to "C2ID"
|
||||
And I press "Save changes"
|
||||
Then "Competency updated" "text" should exist
|
||||
And "C1" "text" should not exist
|
||||
And "C2" "text" should exist
|
||||
And I select "C2" of the competency tree
|
||||
And "C2ID" "text" should exist
|
||||
And "C1ID" "text" should not exist
|
||||
And "C2 description" "text" should exist
|
||||
And "C1 description" "text" should not exist
|
||||
|
||||
@javascript
|
||||
Scenario: Delete a competency
|
||||
Given I select "C1" of the competency tree
|
||||
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
|
||||
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
|
||||
When I click on "Delete" "link"
|
||||
And I click on "Delete" "button" in the "Confirm" "dialogue"
|
||||
# Confirm that "C1" competency was successfully deleted.
|
||||
Then "C1" "text" should not exist
|
||||
@@ -0,0 +1,63 @@
|
||||
@tool @tool_lp @tool_lp_framework
|
||||
Feature: Move and cross-reference competencies
|
||||
In order to move and cross-reference competencies
|
||||
As a manager
|
||||
I need to be open the competency's menu items.
|
||||
|
||||
Background:
|
||||
Given the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| CF1 | CF1 |
|
||||
And the following "core_competency > competencies" exist:
|
||||
| shortname | competencyframework |
|
||||
| C1 | CF1 |
|
||||
| C2 | CF1 |
|
||||
| C3 | CF1 |
|
||||
| C4 | CF1 |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Competencies > Competency frameworks" in site administration
|
||||
And I click on "CF1 (CF1)" "link"
|
||||
|
||||
@javascript
|
||||
Scenario: Move a competency using Move up/Move down menu items
|
||||
Given I select "C1" of the competency tree
|
||||
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
|
||||
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
|
||||
When I click on "Move down" "link"
|
||||
Then "C1" "text" should appear after "C2" "text"
|
||||
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
|
||||
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
|
||||
And I click on "Move up" "link"
|
||||
And "C1" "text" should appear before "C2" "text"
|
||||
|
||||
@javascript
|
||||
Scenario: Move a competency using Relocate menu item
|
||||
Given I select "C3" of the competency tree
|
||||
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
|
||||
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
|
||||
When I click on "Relocate" "link"
|
||||
And I click on "C1" "text" in the "Move competency" "dialogue"
|
||||
And I click on "Move" "button" in the "Move competency" "dialogue"
|
||||
Then "C1" "text" should appear before "C3" "text"
|
||||
And "C3" "text" should appear before "C2" "text"
|
||||
And I select "C3" of the competency tree
|
||||
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
|
||||
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
|
||||
# Similar to the previous step, to avoid ambiguity with the competency framework "Edit", target css element with data-action=edit.
|
||||
And I click on "[data-action=edit]" "css_element"
|
||||
And "C1" "text" should exist
|
||||
And "No parent (top-level competency)" "text" should not exist
|
||||
And I press "Cancel"
|
||||
And "C1" "text" should appear before "C3" "text"
|
||||
And "C3" "text" should appear before "C2" "text"
|
||||
|
||||
@javascript
|
||||
Scenario: Cross-reference a competency
|
||||
Given I select "C1" of the competency tree
|
||||
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
|
||||
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
|
||||
When I click on "Add cross-referenced competency" "link"
|
||||
And I click on "C2" "text" in the "Competency picker" "dialogue"
|
||||
And I click on "Add" "button" in the "Competency picker" "dialogue"
|
||||
Then "Cross-referenced competencies:" "text" should exist
|
||||
And I should see "C2 cmp2"
|
||||
@@ -4,13 +4,13 @@ Feature: See the competencies for an activity on the course competencies page.
|
||||
In order to see only the competencies for an activity in the course competencies page.
|
||||
|
||||
Background:
|
||||
Given the following lp "frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| Test-Framework | ID-FW1 |
|
||||
And the following lp "competencies" exist:
|
||||
| shortname | framework |
|
||||
| Test-Comp1 | ID-FW1 |
|
||||
| Test-Comp2 | ID-FW1 |
|
||||
Given the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| Test-Framework | ID-FW1 |
|
||||
And the following "core_competency > competencies" exist:
|
||||
| shortname | competencyframework |
|
||||
| Test-Comp1 | ID-FW1 |
|
||||
| Test-Comp2 | ID-FW1 |
|
||||
Given the following "courses" exist:
|
||||
| shortname | fullname | enablecompletion |
|
||||
| C1 | Course 1 | 1 |
|
||||
@@ -46,11 +46,11 @@ Feature: See the competencies for an activity on the course competencies page.
|
||||
When I follow "Competencies"
|
||||
Then I should see "Test-Comp1"
|
||||
And I should see "Test-Comp2"
|
||||
And I set the field "Filter competencies by resource or activity" to "PageName1"
|
||||
And I set the competency filter "Filter competencies by resource or activity" to "PageName1"
|
||||
And I press the enter key
|
||||
And I should see "Test-Comp1"
|
||||
And I should not see "Test-Comp2"
|
||||
And I set the field "Filter competencies by resource or activity" to "PageName2"
|
||||
And I set the competency filter "Filter competencies by resource or activity" to "PageName2"
|
||||
And I press the enter key
|
||||
And I should not see "Test-Comp1"
|
||||
And I should not see "Test-Comp2"
|
||||
|
||||
@@ -29,18 +29,18 @@ Feature: Manage competency frameworks
|
||||
And I should see "Science Year-1"
|
||||
|
||||
Scenario: Read a framework
|
||||
Given the following lp "frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| Science Year-2 | sc-y-2 |
|
||||
Given the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| Science Year-2 | sc-y-2 |
|
||||
And I navigate to "Competencies > Competency frameworks" in site administration
|
||||
And I should see "Science Year-2"
|
||||
When I click on "Science Year-2" "link"
|
||||
Then I should see "Science Year-2"
|
||||
|
||||
Scenario: Edit a framework
|
||||
Given the following lp "frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| Science Year-3 | sc-y-3 |
|
||||
Given the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| Science Year-3 | sc-y-3 |
|
||||
And I navigate to "Competencies > Competency frameworks" in site administration
|
||||
And I should see "Science Year-3"
|
||||
And I click on "Edit" of edit menu in the "Science Year-3" row
|
||||
@@ -52,9 +52,9 @@ Feature: Manage competency frameworks
|
||||
And I should see "sc-y-3"
|
||||
|
||||
Scenario: Delete a framework
|
||||
Given the following lp "frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| Science Year-4 | sc-y-4 |
|
||||
Given the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| Science Year-4 | sc-y-4 |
|
||||
And I navigate to "Competencies > Competency frameworks" in site administration
|
||||
And I should see "Science Year-4"
|
||||
And I should see "sc-y-4"
|
||||
@@ -70,24 +70,24 @@ Feature: Manage competency frameworks
|
||||
And I should not see "sc-y-4"
|
||||
|
||||
Scenario: Edit a framework with competencies in user competency
|
||||
Given the following lp "frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| Science Year-5 | sc-y-5 |
|
||||
And the following lp "competencies" exist:
|
||||
| shortname | framework |
|
||||
| Comp1 | sc-y-5 |
|
||||
| Comp2 | sc-y-5 |
|
||||
And the following lp "plans" exist:
|
||||
| name | user | description |
|
||||
Given the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| Science Year-5 | sc-y-5 |
|
||||
And the following "core_competency > competencies" exist:
|
||||
| shortname | competencyframework | idnumber |
|
||||
| Comp1 | sc-y-5 | Comp1 |
|
||||
| Comp2 | sc-y-5 | Comp2 |
|
||||
And the following "core_competency > plans" exist:
|
||||
| name | user | description |
|
||||
| Plan Science-5 | admin | Plan description |
|
||||
And the following lp "plancompetencies" exist:
|
||||
| plan | competency |
|
||||
| Plan Science-5 | Comp1 |
|
||||
| Plan Science-5 | Comp2 |
|
||||
And the following lp "usercompetencies" exist:
|
||||
| user | competency |
|
||||
| admin | Comp1 |
|
||||
| admin | Comp2 |
|
||||
And the following "core_competency > plan_competency" exist:
|
||||
| plan | competency |
|
||||
| Plan Science-5 | Comp1 |
|
||||
| Plan Science-5 | Comp2 |
|
||||
And the following "core_competency > user_competency" exist:
|
||||
| user | competency |
|
||||
| admin | Comp1 |
|
||||
| admin | Comp2 |
|
||||
And I navigate to "Competencies > Competency frameworks" in site administration
|
||||
And I should see "Science Year-5"
|
||||
And I click on "Edit" of edit menu in the "Science Year-5" row
|
||||
@@ -100,24 +100,24 @@ Feature: Manage competency frameworks
|
||||
And I should see "sc-y-5"
|
||||
|
||||
Scenario: Edit a framework with competencies in user competency plan
|
||||
Given the following lp "frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| Science Year-6 | sc-y-6 |
|
||||
And the following lp "competencies" exist:
|
||||
| shortname | framework |
|
||||
| Comp1 | sc-y-6 |
|
||||
| Comp2 | sc-y-6 |
|
||||
And the following lp "plans" exist:
|
||||
| name | user | description |
|
||||
Given the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| Science Year-6 | sc-y-6 |
|
||||
And the following "core_competency > competencies" exist:
|
||||
| shortname | competencyframework | idnumber |
|
||||
| Comp1 | sc-y-6 | Comp1 |
|
||||
| Comp2 | sc-y-6 | Comp2 |
|
||||
And the following "core_competency > plans" exist:
|
||||
| name | user | description |
|
||||
| Plan Science-6 | admin | Plan description |
|
||||
And the following lp "plancompetencies" exist:
|
||||
| plan | competency |
|
||||
| Plan Science-6 | Comp1 |
|
||||
| Plan Science-6 | Comp2 |
|
||||
And the following lp "usercompetencyplans" exist:
|
||||
| user | competency | plan |
|
||||
| admin | Comp1 | Plan Science-6 |
|
||||
| admin | Comp2 | Plan Science-6 |
|
||||
And the following "core_competency > plan_competency" exist:
|
||||
| plan | competency |
|
||||
| Plan Science-6 | Comp1 |
|
||||
| Plan Science-6 | Comp2 |
|
||||
And the following "core_competency > user_competency_plans" exist:
|
||||
| user | competency | plan |
|
||||
| admin | Comp1 | Plan Science-6 |
|
||||
| admin | Comp2 | Plan Science-6 |
|
||||
And I navigate to "Competencies > Competency frameworks" in site administration
|
||||
And I should see "Science Year-6"
|
||||
And I click on "Edit" of edit menu in the "Science Year-6" row
|
||||
@@ -130,12 +130,12 @@ Feature: Manage competency frameworks
|
||||
And I should see "sc-y-6"
|
||||
|
||||
Scenario: Duplicate a competency framework
|
||||
Given the following lp "frameworks" exist:
|
||||
Given the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| CF1 | CF1 |
|
||||
And the following lp "competencies" exist:
|
||||
| shortname | framework |
|
||||
| C1 | CF1 |
|
||||
And the following "core_competency > competencies" exist:
|
||||
| shortname | competencyframework |
|
||||
| C1 | CF1 |
|
||||
And I navigate to "Competencies > Competency frameworks" in site administration
|
||||
# Duplicate the selected competency framework
|
||||
When I click on "Duplicate" of edit menu in the "CF1" row
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
@tool @tool_lp @tool_lp_plan
|
||||
Feature: Admin can create learning plans from template
|
||||
In order to link and unlink a learning plan from a student
|
||||
As an admin
|
||||
I need to be able to create a learning plan from template
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| student1 | Student | One | student1@email.com |
|
||||
And the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| CF1 | CF1 |
|
||||
| CF2 | CF2 |
|
||||
And the following "core_competency > competencies" exist:
|
||||
| shortname | competencyframework | idnumber |
|
||||
| CF1 Competency 1 | CF1 | CF1C1 |
|
||||
| CF2 Competency 1 | CF2 | CF2C1 |
|
||||
And the following "core_competency > templates" exist:
|
||||
| shortname | description |
|
||||
| LPT1 | LPT 1 description |
|
||||
And the following "core_competency > template_competencies" exist:
|
||||
| template | competency |
|
||||
| LPT1 | CF1C1 |
|
||||
|
||||
@javascript
|
||||
Scenario: Admin can assign competencies from different frameworks to a learning plan
|
||||
Given I log in as "admin"
|
||||
And I navigate to "Competencies > Learning plan templates" in site administration
|
||||
And I follow "LPT1"
|
||||
When I press "Add competencies to learning plan template"
|
||||
# Add another competency from a different framework in the learning plan template.
|
||||
And I set the field with xpath "//select[@data-action='chooseframework']" to "CF2 CF2"
|
||||
And I select "CF2 Competency 1" of the competency tree
|
||||
And I click on "Add" "button" in the "Competency picker" "dialogue"
|
||||
# Confirm that both competencies from different frameworks are successfully added to learning plan template.
|
||||
Then "LPT1" "text" should exist
|
||||
And "Learning plan template competencies" "text" should exist
|
||||
And "CF1 Competency 1" "text" should exist
|
||||
And "CF2 Competency 1" "text" should exist
|
||||
|
||||
@javascript
|
||||
Scenario: Admin can link and unlink learning plans created from template
|
||||
Given I log in as "admin"
|
||||
And I navigate to "Competencies > Learning plan templates" in site administration
|
||||
# Initially, the number of learning plans is 0.
|
||||
And the following should exist in the "generaltable" table:
|
||||
| Name | Learning plans |
|
||||
| LPT1 | 0 |
|
||||
And I click on ".template-userplans" "css_element" in the "LPT1" "table_row"
|
||||
# Create a learning plan for selected student using template.
|
||||
When I set the field "Select users" to "student1"
|
||||
And I press "Create learning plans"
|
||||
# Confirm that selected student is now in the list of learning plans.
|
||||
Then "A learning plan was created" "text" should exist
|
||||
And the following should exist in the "generaltable" table:
|
||||
| Name | Email address |
|
||||
| LPT1 | student1@email.com |
|
||||
And I click on "LPT1" "link" in the "LPT1" "table_row"
|
||||
# Template title and Unlink from learning plan template link exists.
|
||||
And "Learning plan template" "text" should exist
|
||||
And "Unlink from learning plan template" "link" should exist
|
||||
# Unlink the template.
|
||||
And I click on "Unlink from learning plan template" "link"
|
||||
And I press "Unlink from learning plan template"
|
||||
# Learning plan still exists but Template name and Unlink from learning plan template link no longer exist.
|
||||
And "LPT1" "text" should exist
|
||||
And "Learning plan template" "text" should not exist
|
||||
And "Unlink from learning plan template" "link" should not exist
|
||||
@@ -22,8 +22,8 @@ Feature: Manage plearning plan
|
||||
And I should see "Science plan"
|
||||
|
||||
Scenario: Create a learning plan based on template
|
||||
Given the following lp "templates" exist:
|
||||
| shortname | description |
|
||||
Given the following "core_competency > templates" exist:
|
||||
| shortname | description |
|
||||
| Science template | science template description |
|
||||
And I am on homepage
|
||||
And I navigate to "Competencies > Learning plan templates" in site administration
|
||||
@@ -36,8 +36,8 @@ Feature: Manage plearning plan
|
||||
And I should see "Admin User" in the "Science template" "table_row"
|
||||
|
||||
Scenario: Create a learning plan from template cohort
|
||||
Given the following lp "templates" exist:
|
||||
| shortname | description |
|
||||
Given the following "core_competency > templates" exist:
|
||||
| shortname | description |
|
||||
| Science template cohort | science template description |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
@@ -62,8 +62,8 @@ Feature: Manage plearning plan
|
||||
And I should see "Student 2"
|
||||
|
||||
Scenario: Read a learning plan
|
||||
Given the following lp "plans" exist:
|
||||
| name | user | description |
|
||||
Given the following "core_competency > plans" exist:
|
||||
| name | user | description |
|
||||
| Science plan Year-1 | admin | science plan description |
|
||||
And I follow "Learning plans"
|
||||
And I should see "Science plan Year-1"
|
||||
@@ -72,16 +72,16 @@ Feature: Manage plearning plan
|
||||
And I should see "Learning plan competencies"
|
||||
|
||||
Scenario: Manage a learning plan competencies
|
||||
Given the following lp "plans" exist:
|
||||
| name | user | description |
|
||||
Given the following "core_competency > plans" exist:
|
||||
| name | user | description |
|
||||
| Science plan Year-manage | admin | science plan description |
|
||||
And the following lp "frameworks" exist:
|
||||
And the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| Framework 1 | sc-y-2 |
|
||||
And the following lp "competencies" exist:
|
||||
| shortname | framework |
|
||||
| comp1 | sc-y-2 |
|
||||
| comp2 | sc-y-2 |
|
||||
And the following "core_competency > competencies" exist:
|
||||
| shortname | competencyframework |
|
||||
| comp1 | sc-y-2 |
|
||||
| comp2 | sc-y-2 |
|
||||
And I follow "Learning plans"
|
||||
And I should see "Science plan Year-manage"
|
||||
And I follow "Science plan Year-manage"
|
||||
@@ -98,8 +98,8 @@ Feature: Manage plearning plan
|
||||
And "comp1" "table_row" should not exist
|
||||
|
||||
Scenario: Edit a learning plan
|
||||
Given the following lp "plans" exist:
|
||||
| name | user | description |
|
||||
Given the following "core_competency > plans" exist:
|
||||
| name | user | description |
|
||||
| Science plan Year-2 | admin | science plan description |
|
||||
| Science plan Year-3 | admin | science plan description |
|
||||
And I follow "Learning plans"
|
||||
@@ -113,8 +113,8 @@ Feature: Manage plearning plan
|
||||
And I should see "Science plan Year-3 Edited"
|
||||
|
||||
Scenario: Delete a learning plan
|
||||
Given the following lp "plans" exist:
|
||||
| name | user | description |
|
||||
Given the following "core_competency > plans" exist:
|
||||
| name | user | description |
|
||||
| Science plan Year-4 | admin | science plan description |
|
||||
And I follow "Learning plans"
|
||||
And I should see "Science plan Year-4"
|
||||
@@ -130,16 +130,16 @@ Feature: Manage plearning plan
|
||||
Then I should not see "Science plan Year-4"
|
||||
|
||||
Scenario: See a learning plan from a course
|
||||
Given the following lp "plans" exist:
|
||||
| name | user | description |
|
||||
Given the following "core_competency > plans" exist:
|
||||
| name | user | description |
|
||||
| Science plan Year-manage | admin | science plan description |
|
||||
And the following lp "frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| Framework 1 | sc-y-2 |
|
||||
And the following lp "competencies" exist:
|
||||
| shortname | framework |
|
||||
| comp1 | sc-y-2 |
|
||||
| comp2 | sc-y-2 |
|
||||
And the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| Framework 1 | sc-y-2 |
|
||||
And the following "core_competency > competencies" exist:
|
||||
| shortname | competencyframework |
|
||||
| comp1 | sc-y-2 |
|
||||
| comp2 | sc-y-2 |
|
||||
And I follow "Learning plans"
|
||||
And I should see "Science plan Year-manage"
|
||||
And I follow "Science plan Year-manage"
|
||||
|
||||
@@ -16,34 +16,28 @@ Feature: Manage plan workflow
|
||||
| usermanageownplan | User manage own plan role | user |
|
||||
| manageplan | Manager all plans role | manager |
|
||||
And the following "role capabilities" exist:
|
||||
| role | moodle/competency:planmanageowndraft | moodle/competency:planmanageown |
|
||||
| usermanageowndraftplan | allow | |
|
||||
| usermanageownplan | allow | allow |
|
||||
| manageplan | allow | allow |
|
||||
And the following "role capability" exists:
|
||||
| role | manageplan |
|
||||
| moodle/competency:planmanage | allow |
|
||||
| moodle/competency:planview | allow |
|
||||
| moodle/competency:planreview | allow |
|
||||
| moodle/competency:planrequestreview | allow |
|
||||
| role | moodle/competency:planmanageowndraft | moodle/competency:planmanageown | moodle/competency:planmanage | moodle/competency:planview | moodle/competency:planreview | moodle/competency:planrequestreview |
|
||||
| usermanageowndraftplan | allow | | | | | |
|
||||
| usermanageownplan | allow | allow | | | | |
|
||||
| manageplan | allow | allow | allow | allow | allow | allow |
|
||||
And the following "role assigns" exist:
|
||||
| user | role | contextlevel | reference |
|
||||
| user1 | usermanageowndraftplan | System | |
|
||||
| user2 | usermanageownplan | System | |
|
||||
| manager1 | manageplan | System | |
|
||||
And the following lp "frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| Test-Framework | ID-FW1 |
|
||||
And the following lp "competencies" exist:
|
||||
| shortname | framework |
|
||||
| Test-Comp1 | ID-FW1 |
|
||||
| Test-Comp2 | ID-FW1 |
|
||||
And the following lp "plans" exist:
|
||||
| name | user | description |
|
||||
| Test-Plan1 | user1 | Description of plan for user 1 |
|
||||
| Test-Plan2 | user2 | Description of plan for user 2 |
|
||||
And the following lp "plancompetencies" exist:
|
||||
| plan | competency |
|
||||
And the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| Test-Framework | ID-FW1 |
|
||||
And the following "core_competency > competencies" exist:
|
||||
| shortname | competencyframework | idnumber |
|
||||
| Test-Comp1 | ID-FW1 | Test-Comp1 |
|
||||
| Test-Comp2 | ID-FW1 | Test-Comp2 |
|
||||
And the following "core_competency > plans" exist:
|
||||
| name | user | description | reviewer | status |
|
||||
| Test-Plan1 | user1 | Description of plan for user 1 | user1 | draft |
|
||||
| Test-Plan2 | user2 | Description of plan for user 2 | user2 | draft |
|
||||
And the following "core_competency > plan_competency" exist:
|
||||
| plan | competency |
|
||||
| Test-Plan1 | Test-Comp1 |
|
||||
| Test-Plan1 | Test-Comp2 |
|
||||
| Test-Plan2 | Test-Comp1 |
|
||||
@@ -53,8 +47,7 @@ Feature: Manage plan workflow
|
||||
| lp | System | 1 | my-index | content |
|
||||
|
||||
Scenario: User can manages his own plan draft
|
||||
Given I log in as "user1"
|
||||
And I follow "Profile" in the user menu
|
||||
Given I am on the "user1" "user > profile" page logged in as user1
|
||||
When I follow "Learning plans"
|
||||
Then I should see "List of learning plans"
|
||||
And I should see "Test-Plan1"
|
||||
@@ -63,11 +56,9 @@ Feature: Manage plan workflow
|
||||
And I should see "Waiting for review"
|
||||
And I click on "Cancel review" of edit menu in the "Test-Plan1" row
|
||||
And I should see "Draft"
|
||||
And I log out
|
||||
|
||||
Scenario: User can manages his own plan
|
||||
Given I log in as "user2"
|
||||
And I follow "Profile" in the user menu
|
||||
Given I am on the "user2" "user > profile" page logged in as user2
|
||||
When I follow "Learning plans"
|
||||
Then I should see "List of learning plans"
|
||||
And I should see "Test-Plan2"
|
||||
@@ -82,25 +73,24 @@ Feature: Manage plan workflow
|
||||
And I should see "Active"
|
||||
And I click on "Complete this learning plan" of edit menu in the "Test-Plan2" row
|
||||
And I click on "Complete this learning plan" "button" in the "Confirm" "dialogue"
|
||||
And I wait until the page is ready
|
||||
And I should see "Complete"
|
||||
And I click on "Reopen this learning plan" of edit menu in the "Test-Plan2" row
|
||||
And I click on "Reopen this learning plan" "button" in the "Confirm" "dialogue"
|
||||
And I should see "Active"
|
||||
And I log out
|
||||
|
||||
Scenario: Manager can see learning plan with status waiting for review
|
||||
Given the following lp "plans" exist:
|
||||
| name | user | description | status |
|
||||
Given the following "core_competency > plans" exist:
|
||||
| name | user | description | status |
|
||||
| Test-Plan3 | user2 | Description of plan 3 for user 1 | waiting for review |
|
||||
| Test-Plan4 | user1 | Description of plan 3 for user 1 | draft |
|
||||
| Test-Plan4 | user1 | Description of plan 3 for user 1 | draft |
|
||||
When I log in as "manager1"
|
||||
Then I should see "Test-Plan3"
|
||||
And I should not see "Test-Plan4"
|
||||
And I log out
|
||||
|
||||
Scenario: Manager can start review of learning plan with status waiting for review
|
||||
Given the following lp "plans" exist:
|
||||
| name | user | description | status |
|
||||
Given the following "core_competency > plans" exist:
|
||||
| name | user | description | status |
|
||||
| Test-Plan3 | user1 | Description of plan 3 for user 1 | waiting for review |
|
||||
And I log in as "manager1"
|
||||
And I follow "Test-Plan3"
|
||||
@@ -108,11 +98,10 @@ Feature: Manage plan workflow
|
||||
And I should see "Test-Plan3"
|
||||
When I follow "Start review"
|
||||
Then I should see "In review"
|
||||
And I log out
|
||||
|
||||
Scenario: Manager can reject a learning plan with status in review
|
||||
Given the following lp "plans" exist:
|
||||
| name | user | description | status | reviewer |
|
||||
Given the following "core_competency > plans" exist:
|
||||
| name | user | description | status | reviewer |
|
||||
| Test-Plan3 | user1 | Description of plan 3 for user 1 | in review | manager1 |
|
||||
And I log in as "manager1"
|
||||
And I follow "Test-Plan3"
|
||||
@@ -121,11 +110,10 @@ Feature: Manage plan workflow
|
||||
And I should see "In review"
|
||||
When I follow "Finish review"
|
||||
Then I should see "Draft"
|
||||
And I log out
|
||||
|
||||
Scenario: Manager can accept a learning plan with status in review
|
||||
Given the following lp "plans" exist:
|
||||
| name | user | description | status | reviewer |
|
||||
Given the following "core_competency > plans" exist:
|
||||
| name | user | description | status | reviewer |
|
||||
| Test-Plan3 | user1 | Description of plan 3 for user 1 | in review | manager1 |
|
||||
And I log in as "manager1"
|
||||
And I follow "Test-Plan3"
|
||||
@@ -134,11 +122,10 @@ Feature: Manage plan workflow
|
||||
And I should see "In review"
|
||||
When I follow "Make active"
|
||||
Then I should see "Active"
|
||||
And I log out
|
||||
|
||||
Scenario: Manager send back to draft an active learning plan
|
||||
Given the following lp "plans" exist:
|
||||
| name | user | description | status | reviewer |
|
||||
Given the following "core_competency > plans" exist:
|
||||
| name | user | description | status | reviewer |
|
||||
| Test-Plan3 | user1 | Description of plan 3 for user 1 | active | manager1 |
|
||||
| Test-Plan4 | user1 | Description of plan 4 for user 1 | active | manager1 |
|
||||
And I am on the "user1" "user > profile" page logged in as "manager1"
|
||||
@@ -150,11 +137,10 @@ Feature: Manage plan workflow
|
||||
And I follow "Learning plans"
|
||||
Then I should see "Draft"
|
||||
And I should not see "Active"
|
||||
And I log out
|
||||
|
||||
Scenario: Manager change an active learning plan to completed
|
||||
Given the following lp "plans" exist:
|
||||
| name | user | description | status | reviewer |
|
||||
Given the following "core_competency > plans" exist:
|
||||
| name | user | description | status | reviewer |
|
||||
| Test-Plan3 | user1 | Description of plan 3 for user 1 | active | manager1 |
|
||||
| Test-Plan4 | user1 | Description of plan 4 for user 1 | active | manager1 |
|
||||
And I am on the "user1" "user > profile" page logged in as "manager1"
|
||||
@@ -169,11 +155,10 @@ Feature: Manage plan workflow
|
||||
And I follow "Learning plans"
|
||||
Then I should see "Complete"
|
||||
And I should not see "Active"
|
||||
And I log out
|
||||
|
||||
Scenario: Manager reopen a complete learning plan
|
||||
Given the following lp "plans" exist:
|
||||
| name | user | description | status | reviewer |
|
||||
Given the following "core_competency > plans" exist:
|
||||
| name | user | description | status | reviewer |
|
||||
| Test-Plan3 | user1 | Description of plan 3 for user 1 | complete | manager1 |
|
||||
| Test-Plan4 | user1 | Description of plan 4 for user 1 | complete | manager1 |
|
||||
And I am on the "user1" "user > profile" page logged in as "manager1"
|
||||
@@ -187,4 +172,67 @@ Feature: Manage plan workflow
|
||||
And I follow "Learning plans"
|
||||
Then I should see "Active"
|
||||
And I should not see "Complete"
|
||||
And I log out
|
||||
|
||||
Scenario: Student learning plan derived from templates can be completed
|
||||
Given the following "core_competency > templates" exist:
|
||||
| shortname |
|
||||
| LPT1 |
|
||||
And the following "core_competency > template_competencies" exist:
|
||||
| template | competency |
|
||||
| LPT1 | Test-Comp1 |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Competencies > Learning plan templates" in site administration
|
||||
# Select 1 user to assign to create learning plans for using template.
|
||||
And I click on ".template-userplans" "css_element" in the "LPT1" "table_row"
|
||||
And I set the field "Select users" to "user1"
|
||||
And I press "Create learning plans"
|
||||
And I click on "LPT1" "link" in the "LPT1" "table_row"
|
||||
When I click on "Complete this learning plan" "link"
|
||||
And I click on "Complete this learning plan" "button" in the "Confirm" "dialogue"
|
||||
# Add a short wait to ensure the page has loaded before checking that "Complete" "text" exists.
|
||||
And I wait until the page is ready
|
||||
# Confirm that student's learning plan template is marked as Completed.
|
||||
Then "Complete" "text" should exist
|
||||
And "Reopen this learning plan" "link" should exist
|
||||
|
||||
Scenario: Learning plan template updates are not reflected on plans already completed
|
||||
Given the following "core_competency > templates" exist:
|
||||
| shortname |
|
||||
| LPT1 |
|
||||
And the following "core_competency > template_competencies" exist:
|
||||
| template | competency |
|
||||
| LPT1 | Test-Comp1 |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Competencies > Learning plan templates" in site administration
|
||||
# Select 2 users to assign to create learning plans for using template.
|
||||
And I click on ".template-userplans" "css_element" in the "LPT1" "table_row"
|
||||
And I set the field "Select users" to "user1"
|
||||
And I press "Create learning plans"
|
||||
And I set the field "Select users" to "user2"
|
||||
And I press "Create learning plans"
|
||||
# Complete the learning plan for User 1.
|
||||
And I click on "LPT1" "link" in the "User 1" "table_row"
|
||||
And I click on "Complete this learning plan" "link"
|
||||
And I click on "Complete this learning plan" "button" in the "Confirm" "dialogue"
|
||||
# Add another competency to the learning plan template.
|
||||
And the following "core_competency > template_competencies" exist:
|
||||
| template | competency |
|
||||
| LPT1 | Test-Comp2 |
|
||||
# Navigate back to the list of Learning plan templates in order to access User 1's learning plan.
|
||||
And I navigate to "Competencies > Learning plan templates" in site administration
|
||||
And I click on ".template-userplans" "css_element" in the "LPT1" "table_row"
|
||||
# Confirm that only the first competency is reflected on User 1's learning plan since it's already completed.
|
||||
When I click on "LPT1" "link" in the "User 1" "table_row"
|
||||
Then "Test-Comp2" "link" should not exist
|
||||
And "Test-Comp2" "text" should not exist
|
||||
And "Test-Comp1" "link" should exist
|
||||
And "Test-Comp1" "text" should exist
|
||||
# Navigate back to list of Learning plan templates in order to access User 2's learning plan.
|
||||
And I navigate to "Competencies > Learning plan templates" in site administration
|
||||
And I click on ".template-userplans" "css_element" in the "LPT1" "table_row"
|
||||
# Confirm that both competencies are reflected on User 2's learning plan since it's not yet completed.
|
||||
And I click on "LPT1" "link" in the "User 2" "table_row"
|
||||
And "Test-Comp2" "link" should exist
|
||||
And "Test-Comp2" "text" should exist
|
||||
And "Test-Comp1" "link" should exist
|
||||
And "Test-Comp1" "text" should exist
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
@tool @tool_lp @core_cohort
|
||||
Feature: Cohorts can be synchronized with learning plans
|
||||
In order to create learning plans for cohort members
|
||||
As an admin
|
||||
I need to be able to synchronise cohorts with learning plans
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| user1 | User | One | user1@example.com |
|
||||
| user2 | User | Two | user2@example.com |
|
||||
And the following "cohorts" exist:
|
||||
| name | idnumber |
|
||||
| Cohort 1 | CH1 |
|
||||
And the following "cohort members" exist:
|
||||
| user | cohort |
|
||||
| user1 | CH1 |
|
||||
| user2 | CH1 |
|
||||
And the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| CF1 | CF1 |
|
||||
And the following "core_competency > competencies" exist:
|
||||
| shortname | competencyframework | idnumber |
|
||||
| C1 | CF1 | C1 |
|
||||
And the following "core_competency > templates" exist:
|
||||
| shortname |
|
||||
| LPT1 |
|
||||
And the following "core_competency > template_competencies" exist:
|
||||
| template | competency |
|
||||
| LPT1 | C1 |
|
||||
|
||||
@javascript
|
||||
Scenario: Cohorts can be synchronised with learning plans
|
||||
Given I log in as "admin"
|
||||
# Navigate to the list of learning plan templates in order to add cohorts.
|
||||
And I navigate to "Competencies > Learning plan templates" in site administration
|
||||
When I click on "Add cohorts to sync" of edit menu in the "LPT1" row
|
||||
And I set the field "Select cohorts to sync" to "Cohort 1"
|
||||
And I press "Add cohorts"
|
||||
And I wait until the page is ready
|
||||
# Confirm that 2 learning plans were created for members of the cohort.
|
||||
Then "2 learning plans were created" "text" should exist
|
||||
# Confirm current screen is still "Cohorts synced to this learning plan template screen"
|
||||
And "Cohorts synced to this learning plan template" "text" should exist
|
||||
# Confirm that the cohort is now added to the learning plan template.
|
||||
And the following should exist in the "generaltable" table:
|
||||
| Name | Cohort ID |
|
||||
| Cohort 1 | CH1 |
|
||||
# Navigate back to the list of learning plan templates to view updated list.
|
||||
And I navigate to "Competencies > Learning plan templates" in site administration
|
||||
# Confirm that the added cohort and learning plans are now reflected on the list of Learning plan templates.
|
||||
And the following should exist in the "generaltable" table:
|
||||
| Name | Category | Cohorts | Learning plans |
|
||||
| LPT1 | System | 1 | 2 |
|
||||
And I click on ".template-userplans" "css_element" in the "LPT1" "table_row"
|
||||
# Confirm that learning plans were created for all cohort members.
|
||||
And the following should exist in the "generaltable" table:
|
||||
| Name | First name | Email address |
|
||||
| LPT1 | User One | user1@example.com |
|
||||
| LPT1 | User Two | user2@example.com |
|
||||
@@ -21,8 +21,8 @@ Feature: Manage plearning plan templates
|
||||
And I should see "Science template"
|
||||
|
||||
Scenario: Read a learning plan template
|
||||
Given the following lp "templates" exist:
|
||||
| shortname | description |
|
||||
Given the following "core_competency > templates" exist:
|
||||
| shortname | description |
|
||||
| Science template Year-2 | science template description |
|
||||
And I navigate to "Competencies > Learning plan templates" in site administration
|
||||
And I should see "Science template Year-2"
|
||||
@@ -31,8 +31,8 @@ Feature: Manage plearning plan templates
|
||||
And I should see "Learning plan template competencies"
|
||||
|
||||
Scenario: Edit a learning plan template
|
||||
Given the following lp "templates" exist:
|
||||
| shortname | description |
|
||||
Given the following "core_competency > templates" exist:
|
||||
| shortname | description |
|
||||
| Science template Year-3 | science template description |
|
||||
And I navigate to "Competencies > Learning plan templates" in site administration
|
||||
And I should see "Science template Year-3"
|
||||
@@ -44,8 +44,8 @@ Feature: Manage plearning plan templates
|
||||
And I should see "Science template Year-3 Edited"
|
||||
|
||||
Scenario: Delete a learning plan template
|
||||
Given the following lp "templates" exist:
|
||||
| shortname | description |
|
||||
Given the following "core_competency > templates" exist:
|
||||
| shortname | description |
|
||||
| Science template Year-4 | science template description |
|
||||
And I navigate to "Competencies > Learning plan templates" in site administration
|
||||
And I should see "Science template Year-4"
|
||||
|
||||
@@ -5,22 +5,22 @@ Feature: Manage competencies linked to evidence of prior learning
|
||||
I need to link and unlink competencies from evidence of prior learning
|
||||
|
||||
Background:
|
||||
Given the following lp "frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| Test-Framework | ID-FW1 |
|
||||
And the following lp "competencies" exist:
|
||||
| shortname | framework |
|
||||
| Test-Comp1 | ID-FW1 |
|
||||
| Test-Comp2 | ID-FW1 |
|
||||
And the following lp "plans" exist:
|
||||
| name | user | description |
|
||||
Given the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| Test-Framework | ID-FW1 |
|
||||
And the following "core_competency > competencies" exist:
|
||||
| shortname | competencyframework | idnumber |
|
||||
| Test-Comp1 | ID-FW1 | Test-Comp1 |
|
||||
| Test-Comp2 | ID-FW1 | Test-Comp2 |
|
||||
And the following "core_competency > plans" exist:
|
||||
| name | user | description |
|
||||
| Test-Plan | admin | Plan description |
|
||||
And the following lp "plancompetencies" exist:
|
||||
| plan | competency |
|
||||
And the following "core_competency > plan_competency" exist:
|
||||
| plan | competency |
|
||||
| Test-Plan | Test-Comp1 |
|
||||
| Test-Plan | Test-Comp2 |
|
||||
And the following lp "userevidence" exist:
|
||||
| name | description | user |
|
||||
And the following "core_competency > user_evidence" exist:
|
||||
| name | description | user |
|
||||
| Test-Evidence | Description evidence | admin |
|
||||
When I log in as "admin"
|
||||
And I follow "Profile" in the user menu
|
||||
@@ -51,12 +51,12 @@ Feature: Manage competencies linked to evidence of prior learning
|
||||
Then "Test-Comp2" "table_row" should exist
|
||||
|
||||
Scenario: Unlink competency from evidence of prior learning
|
||||
Given the following lp "userevidencecompetencies" exist:
|
||||
| userevidence | competency |
|
||||
Given the following "core_competency > user_evidence_competency" exist:
|
||||
| userevidence | competency |
|
||||
| Test-Evidence | Test-Comp1 |
|
||||
| Test-Evidence | Test-Comp2 |
|
||||
Given the following lp "usercompetencies" exist:
|
||||
| user | competency |
|
||||
And the following "core_competency > user_competency" exist:
|
||||
| user | competency |
|
||||
| admin | Test-Comp1 |
|
||||
| admin | Test-Comp2 |
|
||||
And I follow "Evidence of prior learning"
|
||||
|
||||
@@ -24,8 +24,8 @@ Feature: Manage evidence of prior learning
|
||||
And I should see "Evidence-1"
|
||||
|
||||
Scenario: Read an evidence of prior learning
|
||||
Given the following lp "userevidence" exist:
|
||||
| name | description | user |
|
||||
Given the following "core_competency > user_evidence" exist:
|
||||
| name | description | user |
|
||||
| Evidence-2 | Evidence-2 description | user1 |
|
||||
And I follow "Evidence of prior learning"
|
||||
And I should see "List of evidence"
|
||||
@@ -34,8 +34,8 @@ Feature: Manage evidence of prior learning
|
||||
Then I should see "Evidence-2"
|
||||
|
||||
Scenario: Edit an evidence of prior learning
|
||||
Given the following lp "userevidence" exist:
|
||||
| name | description | user |
|
||||
Given the following "core_competency > user_evidence" exist:
|
||||
| name | description | user |
|
||||
| Evidence-3 | Evidence-3 description | user1 |
|
||||
And I follow "Evidence of prior learning"
|
||||
And I should see "List of evidence"
|
||||
@@ -47,8 +47,8 @@ Feature: Manage evidence of prior learning
|
||||
And I should see "Evidence-3 Edited"
|
||||
|
||||
Scenario: Delete an evidence of prior learning
|
||||
Given the following lp "userevidence" exist:
|
||||
| name | description | user |
|
||||
Given the following "core_competency > user_evidence" exist:
|
||||
| name | description | user |
|
||||
| Evidence-4 | Evidence-4 description | user1 |
|
||||
And I follow "Evidence of prior learning"
|
||||
And I should see "List of evidence"
|
||||
@@ -62,10 +62,10 @@ Feature: Manage evidence of prior learning
|
||||
|
||||
Scenario: List evidences of prior learning
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
|user2 | User | 2 | user2@example.com |
|
||||
And the following lp "userevidence" exist:
|
||||
| name | description | user |
|
||||
| username | firstname | lastname | email |
|
||||
| user2 | User | 2 | user2@example.com |
|
||||
And the following "core_competency > user_evidence" exist:
|
||||
| name | description | user |
|
||||
| Evidence-5 | Evidence-5 description | user1 |
|
||||
| Evidence-6 | Evidence-6 description | user2 |
|
||||
And I follow "Evidence of prior learning"
|
||||
|
||||
@@ -40,9 +40,9 @@ Feature: View competencies
|
||||
| competency | relatedcompetency |
|
||||
| desserts | cakes |
|
||||
And the following "core_competency > plans" exist:
|
||||
| name | description | competencies | user |
|
||||
| Cookery | Cookery is important | salads, desserts, cakes | student1 |
|
||||
| Literacy | Literacy is important | reading, writing | student1 |
|
||||
| name | description | competencies | user | status |
|
||||
| Cookery | Cookery is important | salads, desserts, cakes | student1 | active |
|
||||
| Literacy | Literacy is important | reading, writing | student1 | active |
|
||||
And the following "core_competency > course_competencies" exist:
|
||||
| course | competency |
|
||||
| C1 | salads |
|
||||
|
||||
@@ -41,6 +41,18 @@ class manager {
|
||||
/** @var int */
|
||||
const REDIR_LOOP_THRESHOLD = 5;
|
||||
|
||||
/** @var array These components and related fileareas will not redirect. */
|
||||
const ALLOWED_COMPONENTS = [
|
||||
'core_admin' => [
|
||||
'logocompact',
|
||||
'logo',
|
||||
'favicon',
|
||||
],
|
||||
'tool_mfa' => [
|
||||
'guidance',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Displays a debug table with current factor information.
|
||||
*
|
||||
@@ -424,6 +436,37 @@ class manager {
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
|
||||
// Ensure we have a moodle_url object if a string is provided.
|
||||
if (is_string($url)) {
|
||||
$url = new \moodle_url($url);
|
||||
}
|
||||
|
||||
// Check for pluginfile.php urls.
|
||||
$pluginfileurl = new \moodle_url('/pluginfile.php');
|
||||
if ($url->compare($pluginfileurl)) {
|
||||
// Get the slash arguments.
|
||||
$args = explode('/', ltrim($url->get_slashargument(), '/'));
|
||||
|
||||
// Remove the contextid because we do not need it for this check.
|
||||
array_shift($args);
|
||||
|
||||
// Get the component and filearea.
|
||||
$component = clean_param(array_shift($args), PARAM_COMPONENT);
|
||||
$filearea = clean_param(array_shift($args), PARAM_AREA);
|
||||
|
||||
// Check allowed components.
|
||||
if (!array_key_exists($component, static::ALLOWED_COMPONENTS)) {
|
||||
return self::REDIRECT;
|
||||
}
|
||||
|
||||
// Check allowed fileareas.
|
||||
if (!in_array($filearea, static::ALLOWED_COMPONENTS[$component])) {
|
||||
return self::REDIRECT;
|
||||
}
|
||||
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
|
||||
// Remove all params before comparison.
|
||||
$url->remove_all_params();
|
||||
|
||||
@@ -442,12 +485,6 @@ class manager {
|
||||
}
|
||||
}
|
||||
|
||||
// Dont redirect logo images from pluginfile.php (for example: logo in header).
|
||||
$logourl = new \moodle_url('/pluginfile.php/1/core_admin/logocompact/');
|
||||
if ($url->compare($logourl)) {
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
|
||||
// Admin not setup.
|
||||
if (!empty($CFG->adminsetuppending)) {
|
||||
return self::NO_REDIRECT;
|
||||
@@ -469,12 +506,6 @@ class manager {
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
|
||||
// Enrolment.
|
||||
$enrol = new \moodle_url('/enrol/index.php');
|
||||
if ($enrol->compare($url, URL_MATCH_BASE)) {
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
|
||||
// Guest access.
|
||||
if (isguestuser()) {
|
||||
return self::NO_REDIRECT;
|
||||
@@ -500,11 +531,8 @@ class manager {
|
||||
}
|
||||
|
||||
// WS/AJAX check.
|
||||
// Prevents any potential bypassing of multi-factor authentication.
|
||||
if (WS_SERVER || AJAX_SCRIPT) {
|
||||
if (isset($SESSION->mfa_pending) && !empty($SESSION->mfa_pending)) {
|
||||
// Allow AJAX and WS, but never from auth.php.
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
return self::REDIRECT_EXCEPTION;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,8 @@ class factor_email_renderer extends plugin_renderer_base {
|
||||
$authurl = new \moodle_url('/admin/tool/mfa/factor/email/email.php',
|
||||
['instance' => $instance->id, 'pass' => 1, 'secret' => $instance->secret]);
|
||||
$authurlstring = \html_writer::link($authurl, get_string('email:link', 'factor_email'));
|
||||
$blockurl = new \moodle_url('/admin/tool/mfa/factor/email/email.php', ['instance' => $instanceid]);
|
||||
$blockurl = new \moodle_url('/admin/tool/mfa/factor/email/email.php',
|
||||
['instance' => $instance->id, 'secret' => $instance->secret]);
|
||||
$blockurlstring = \html_writer::link($blockurl, get_string('email:stoploginlink', 'factor_email'));
|
||||
$geoinfo = iplookup_find_location($instance->createdfromip);
|
||||
|
||||
|
||||
@@ -42,7 +42,8 @@ $PAGE->set_cacheable(false);
|
||||
$instance = $DB->get_record('tool_mfa', ['id' => $instanceid]);
|
||||
$factor = \tool_mfa\plugininfo\factor::get_factor('email');
|
||||
|
||||
// If pass is set, require login to force $SESSION and user, and pass for that session.
|
||||
// If pass is set, do checks and pass for this session.
|
||||
// Require login to force $SESSION and user, and pass for that session.
|
||||
if (!empty($instance) && $pass != 0 && $secret != 0) {
|
||||
require_login();
|
||||
if ($factor->get_state() === \tool_mfa\plugininfo\factor::STATE_LOCKED) {
|
||||
@@ -69,8 +70,12 @@ $form = new \factor_email\form\email($url);
|
||||
|
||||
if ($form->is_cancelled()) {
|
||||
redirect(new moodle_url('/'));
|
||||
} else if ($fromform = $form->get_data()) {
|
||||
if (empty($instance)) {
|
||||
}
|
||||
|
||||
// If submitted without the pass param, is a cancel request - do checks and revoke email factor.
|
||||
if ($fromform = $form->get_data()) {
|
||||
// Only allow revoke attempts from requests with a valid instance and secret.
|
||||
if (empty($instance) || empty($secret) || $instance->secret != $secret) {
|
||||
$message = get_string('error:badcode', 'factor_email');
|
||||
} else {
|
||||
$user = $DB->get_record('user', ['id' => $instance->userid]);
|
||||
|
||||
@@ -33,7 +33,7 @@ $string['email:loginlink'] = 'Or, if you\'re on the same device, use this {$a}.'
|
||||
$string['email:message'] = 'Here\'s your verification code for {$a->sitename} ({$a->siteurl}).';
|
||||
$string['email:originatingip'] = 'This login request was made from \'{$a}\'';
|
||||
$string['email:revokelink'] = 'If this wasn\'t you, you can {$a}.';
|
||||
$string['email:revokesuccess'] = 'This code has been successfully revoked. All sessions for {$a} have been ended.
|
||||
$string['email:revokesuccess'] = 'This code has been successfully revoked. All sessions for this user have been ended.
|
||||
Email will not be usable as a factor until account security has been verified.';
|
||||
$string['email:subject'] = 'Here\'s your verification code';
|
||||
$string['email:stoploginlink'] = 'stop this login attempt';
|
||||
|
||||
@@ -29,7 +29,7 @@ require_once(__DIR__ . '/../../../config.php');
|
||||
$PAGE->set_context(\context_system::instance());
|
||||
$PAGE->set_url(new moodle_url('/admin/tool/mfa/guide.php'));
|
||||
$PAGE->set_title(get_string('guidance', 'tool_mfa'));
|
||||
$PAGE->set_pagelayout('standard');
|
||||
$PAGE->set_pagelayout('secure');
|
||||
|
||||
// If guidance page isn't enabled, just redir back to home.
|
||||
if (!get_config('tool_mfa', 'guidance')) {
|
||||
|
||||
@@ -109,8 +109,8 @@ function tool_mfa_after_config(): void {
|
||||
/**
|
||||
* Serves any files for the guidance page.
|
||||
*
|
||||
* @param stdClass $course
|
||||
* @param stdClass $cm
|
||||
* @param stdClass|null $course
|
||||
* @param stdClass|null $cm
|
||||
* @param context $context
|
||||
* @param string $filearea
|
||||
* @param array $args
|
||||
@@ -118,7 +118,7 @@ function tool_mfa_after_config(): void {
|
||||
* @param array $options
|
||||
* @return bool
|
||||
*/
|
||||
function tool_mfa_pluginfile(stdClass $course, stdClass $cm, context $context, string $filearea,
|
||||
function tool_mfa_pluginfile(stdClass|null $course, stdClass|null $cm, context $context, string $filearea,
|
||||
array $args, bool $forcedownload, array $options = []): bool {
|
||||
// Hardcode to only send guidance files from the top level.
|
||||
$fs = get_file_storage();
|
||||
|
||||
@@ -23,6 +23,6 @@
|
||||
{
|
||||
}
|
||||
}}
|
||||
<a href='{{config.wwwroot}}/admin/tool/mfa/guide.php'>
|
||||
<a href='{{config.wwwroot}}/admin/tool/mfa/guide.php' target='_blank'>
|
||||
{{#str}} guidance, tool_mfa {{/str}}
|
||||
</a>
|
||||
|
||||
@@ -241,11 +241,6 @@ final class manager_test extends \advanced_testcase {
|
||||
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
|
||||
$this->setUser($user);
|
||||
|
||||
// Enrolment.
|
||||
$enrolurl = new \moodle_url('/enrol/index.php');
|
||||
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
|
||||
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($enrolurl, false));
|
||||
|
||||
// Guest User.
|
||||
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
|
||||
$this->setGuestUser();
|
||||
@@ -266,6 +261,27 @@ final class manager_test extends \advanced_testcase {
|
||||
\core\session\manager::loginas($user2->id, $syscontext, false);
|
||||
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
|
||||
$this->setUser($user);
|
||||
|
||||
// Access logocompact via pluginfile.
|
||||
$logourl = new \moodle_url('/pluginfile.php/1/core_admin/logocompact/');
|
||||
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($logourl, false));
|
||||
|
||||
// Access logo via pluginfile.
|
||||
$logourl = new \moodle_url('/pluginfile.php/1/core_admin/logo/');
|
||||
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($logourl, false));
|
||||
|
||||
// Access favicon via pluginfile.
|
||||
$logourl = new \moodle_url('/pluginfile.php/1/core_admin/favicon/');
|
||||
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($logourl, false));
|
||||
|
||||
// Access guidance files.
|
||||
$guideurl = new \moodle_url('/pluginfile.php/1/tool_mfa/guidance/0/capybara.png');
|
||||
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($guideurl, false));
|
||||
|
||||
// Access private area.
|
||||
$user3 = $this->getDataGenerator()->create_user();
|
||||
$privateurl = new \moodle_url("/pluginfile.php/{$user3->id}/user/private/privatefile.png");
|
||||
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($privateurl, false));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,6 +34,10 @@ if (function_exists('opcache_reset')) {
|
||||
|
||||
define('IGNORE_COMPONENT_CACHE', true);
|
||||
|
||||
// It makes no sense to use BEHAT_CLI for this script (you cannot initialise PHPunit starting from
|
||||
// the Behat environment), so in case user has set tne environment variable, disable it.
|
||||
putenv('BEHAT_CLI=0');
|
||||
|
||||
require_once(__DIR__.'/../../../../lib/clilib.php');
|
||||
require_once(__DIR__.'/../../../../lib/phpunit/bootstraplib.php');
|
||||
require_once(__DIR__.'/../../../../lib/testing/lib.php');
|
||||
|
||||
@@ -30,6 +30,10 @@ if (isset($_SERVER['REMOTE_ADDR'])) {
|
||||
|
||||
define('IGNORE_COMPONENT_CACHE', true);
|
||||
|
||||
// It makes no sense to use BEHAT_CLI for this script (you cannot initialise PHPunit starting from
|
||||
// the Behat environment), so in case user has set tne environment variable, disable it.
|
||||
putenv('BEHAT_CLI=0');
|
||||
|
||||
require_once(__DIR__.'/../../../../lib/clilib.php');
|
||||
require_once(__DIR__.'/../../../../lib/phpunit/bootstraplib.php');
|
||||
require_once(__DIR__.'/../../../../lib/testing/lib.php');
|
||||
|
||||
@@ -42,7 +42,7 @@ $string['activate'] = 'Set status to "Active"';
|
||||
$string['activating'] = 'Activating a policy';
|
||||
$string['activateconfirm'] = '<p>You are about to activate policy <em>\'{$a->name}\'</em> and make the version <em>\'{$a->revision}\'</em> the current one.</p><p>All users will be required to agree to this new policy version to be able to use the site.</p>';
|
||||
$string['activateconfirmyes'] = 'Activate';
|
||||
$string['agreepolicies'] = 'Please agree to the following policies';
|
||||
$string['agreepolicies'] = 'Please agree to the following policies:';
|
||||
$string['backtoprevious'] = 'Go back to previous page';
|
||||
$string['backtotop'] = 'Back to top';
|
||||
$string['cachedef_policy_optional'] = 'Cache of the optional/compulsory flag for policy versions';
|
||||
@@ -74,8 +74,8 @@ $string['filterplaceholder'] = 'Search keyword or select filter';
|
||||
$string['filterpolicy'] = 'Policy: {$a}';
|
||||
$string['guestconsent:continue'] = 'Continue';
|
||||
$string['guestconsentmessage'] = 'If you continue browsing this website, you agree to our policies:';
|
||||
$string['iagree'] = 'I agree to the {$a}';
|
||||
$string['idontagree'] = 'No thanks, I decline {$a}';
|
||||
$string['iagree'] = 'I agree to the {$a}.';
|
||||
$string['idontagree'] = 'I don\'t agree to the {$a}.';
|
||||
$string['iagreetothepolicy'] = 'Give consent';
|
||||
$string['inactivate'] = 'Set status to "Inactive"';
|
||||
$string['inactivating'] = 'Inactivating a policy';
|
||||
@@ -94,9 +94,9 @@ $string['newpolicy'] = 'New policy';
|
||||
$string['newversion'] = 'New version';
|
||||
$string['noactivepolicies'] = 'There are no policies with an active version.';
|
||||
$string['nofiltersapplied'] = 'No filters applied';
|
||||
$string['nopermissiontoagreedocs'] = 'No permission to agree to the policies';
|
||||
$string['nopermissiontoagreedocs'] = 'No permission to agree to the policies.';
|
||||
$string['nopermissiontoagreedocs_desc'] = 'Sorry, you do not have the required permissions to agree to the policies.<br />You will not be able to use this site until the following policies are agreed:';
|
||||
$string['nopermissiontoagreedocsbehalf'] = 'No permission to agree to the policies on behalf of this user';
|
||||
$string['nopermissiontoagreedocsbehalf'] = 'No permission to agree to the policies on behalf of this user.';
|
||||
$string['nopermissiontoagreedocsbehalf_desc'] = 'Sorry, you do not have the required permission to agree to the following policies on behalf of {$a}:';
|
||||
$string['nopermissiontoagreedocscontact'] = 'For more help:';
|
||||
$string['nopermissiontoviewpolicyversion'] = 'You do not have permissions to view this policy version.';
|
||||
|
||||
@@ -34,13 +34,13 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
Then I should see "This site policy"
|
||||
And I should not see "Course overview"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I press "Next"
|
||||
And I should see "Calendar"
|
||||
And I log out
|
||||
And I log in as "manager"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I press "Next"
|
||||
And I navigate to "Users > Privacy and policies > User agreements" in site administration
|
||||
And "Accepted" "text" should exist in the "User One" "table_row"
|
||||
@@ -53,7 +53,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
| tool/policy:acceptbehalf | allow |
|
||||
When I log in as "manager"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I press "Next"
|
||||
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
|
||||
And I click on "1 of 4 (25%)" "link" in the "This site policy" "table_row"
|
||||
@@ -76,7 +76,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
| tool/policy:acceptbehalf | allow |
|
||||
When I log in as "manager"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I press "Next"
|
||||
And I should see "Calendar"
|
||||
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
|
||||
@@ -104,16 +104,16 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
And I press "Next"
|
||||
And I should see "This privacy policy"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "1"
|
||||
And I press "Next"
|
||||
And I should see "Calendar"
|
||||
And I log out
|
||||
And I log in as "manager"
|
||||
And I press "Next"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "1"
|
||||
And I press "Next"
|
||||
And I navigate to "Users > Privacy and policies > User agreements" in site administration
|
||||
And "Accepted" "text" should exist in the "User One" "table_row"
|
||||
@@ -142,8 +142,8 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
When I log in as "manager"
|
||||
And I press "Next"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "1"
|
||||
And I press "Next"
|
||||
And I navigate to "Users > Privacy and policies > User agreements" in site administration
|
||||
And I click on "Accept This site policy" "link" in the "User One" "table_row"
|
||||
@@ -176,8 +176,8 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
When I log in as "manager"
|
||||
And I press "Next"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "1"
|
||||
And I press "Next"
|
||||
And I navigate to "Users > Privacy and policies > User agreements" in site administration
|
||||
And I click on "Accept This site policy" "link" in the "User One" "table_row"
|
||||
@@ -198,7 +198,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
Scenario: Policies and agreements profile link visible for current user
|
||||
Given I log in as "user1"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I press "Next"
|
||||
When I follow "Profile" in the user menu
|
||||
# User can see his own agreements link in the profile.
|
||||
@@ -217,7 +217,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
| tool/policy:acceptbehalf | allow |
|
||||
And I log in as "manager"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I press "Next"
|
||||
# User can see agreements link in other user profiles because has the capability for accepting on behalf of them.
|
||||
When I am on "Course1" course homepage
|
||||
@@ -231,7 +231,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
| tool/policy:acceptbehalf | allow |
|
||||
When I log in as "manager"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I press "Next"
|
||||
And I log out
|
||||
And I am on the "manager" "user > profile" page logged in as "admin"
|
||||
@@ -267,8 +267,8 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
When I log in as "manager"
|
||||
And I press "Next"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "1"
|
||||
And I press "Next"
|
||||
And I navigate to "Users > Privacy and policies > User agreements" in site administration
|
||||
And I click on "Select" "checkbox" in the "User One" "table_row"
|
||||
@@ -290,7 +290,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
And I should see "This site policy"
|
||||
And I should not see "Course overview"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I press "Next"
|
||||
And I should see "Calendar"
|
||||
And I log out
|
||||
|
||||
@@ -4,7 +4,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
As a user
|
||||
I need to be able to accept site policy during sign up
|
||||
|
||||
Scenario: Accept policy on sign up, no site policy
|
||||
Scenario: Accept policy on sign up manage by plugin, no site policy
|
||||
Given the following config values are set as admin:
|
||||
| registerauth | email |
|
||||
| passwordpolicy | 0 |
|
||||
@@ -16,13 +16,13 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I set the following fields to these values:
|
||||
| Username | user1 |
|
||||
| Password | user1 |
|
||||
| Email address | user1@address.invalid |
|
||||
| Email (again) | user1@address.invalid |
|
||||
| Email address | user1@example.com |
|
||||
| Email (again) | user1@example.com |
|
||||
| First name | User1 |
|
||||
| Last name | L1 |
|
||||
And I press "Create my new account"
|
||||
And I should see "Confirm your account"
|
||||
And I should see "An email should have been sent to your address at user1@address.invalid"
|
||||
And I should see "An email should have been sent to your address at user1@example.com"
|
||||
And I confirm email for "user1"
|
||||
And I should see "Thanks, User1 L1"
|
||||
And I should see "Your registration has been confirmed"
|
||||
@@ -50,13 +50,13 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I set the following fields to these values:
|
||||
| Username | user1 |
|
||||
| Password | user1 |
|
||||
| Email address | user1@address.invalid |
|
||||
| Email (again) | user1@address.invalid |
|
||||
| Email address | user1@example.com |
|
||||
| Email (again) | user1@example.com |
|
||||
| First name | User1 |
|
||||
| Last name | L1 |
|
||||
And I press "Create my new account"
|
||||
And I should see "Confirm your account"
|
||||
And I should see "An email should have been sent to your address at user1@address.invalid"
|
||||
And I should see "An email should have been sent to your address at user1@example.com"
|
||||
And I confirm email for "user1"
|
||||
And I should see "Thanks, User1 L1"
|
||||
And I should see "Your registration has been confirmed"
|
||||
@@ -85,23 +85,23 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should see "short text2"
|
||||
And I should see "full text2"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I should see "This site policy" in the "region-main" "region"
|
||||
And I should see "short text2"
|
||||
And I should not see "full text2"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I press "Next"
|
||||
And I should not see "I understand and agree"
|
||||
And I set the following fields to these values:
|
||||
| Username | user1 |
|
||||
| Password | user1 |
|
||||
| Email address | user1@address.invalid |
|
||||
| Email (again) | user1@address.invalid |
|
||||
| Email address | user1@example.com |
|
||||
| Email (again) | user1@example.com |
|
||||
| First name | User1 |
|
||||
| Last name | L1 |
|
||||
And I press "Create my new account"
|
||||
And I should see "Confirm your account"
|
||||
And I should see "An email should have been sent to your address at user1@address.invalid"
|
||||
And I should see "An email should have been sent to your address at user1@example.com"
|
||||
And I confirm email for "user1"
|
||||
And I should see "Thanks, User1 L1"
|
||||
And I should see "Your registration has been confirmed"
|
||||
@@ -137,7 +137,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should see "short text3"
|
||||
And I should see "full text3"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I should see "This site policy" in the "region-main" "region"
|
||||
And I should see "short text2"
|
||||
And I should not see "full text2"
|
||||
@@ -147,20 +147,20 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should not see "This guests policy" in the "region-main" "region"
|
||||
And I should not see "short text4"
|
||||
And I should not see "full text4"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "1"
|
||||
And I press "Next"
|
||||
And I should not see "I understand and agree"
|
||||
And I set the following fields to these values:
|
||||
| Username | user1 |
|
||||
| Password | user1 |
|
||||
| Email address | user1@address.invalid |
|
||||
| Email (again) | user1@address.invalid |
|
||||
| Email address | user1@example.com |
|
||||
| Email (again) | user1@example.com |
|
||||
| First name | User1 |
|
||||
| Last name | L1 |
|
||||
And I press "Create my new account"
|
||||
And I should see "Confirm your account"
|
||||
And I should see "An email should have been sent to your address at user1@address.invalid"
|
||||
And I should see "An email should have been sent to your address at user1@example.com"
|
||||
And I confirm email for "user1"
|
||||
And I should see "Thanks, User1 L1"
|
||||
And I should see "Your registration has been confirmed"
|
||||
@@ -197,23 +197,23 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should see "short text2"
|
||||
And I should see "full text2"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I should see "This site policy"
|
||||
And I should see "short text2"
|
||||
And I should not see "full text2"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I press "Next"
|
||||
And I should not see "I understand and agree"
|
||||
And I set the following fields to these values:
|
||||
| Username | user1 |
|
||||
| Password | user1 |
|
||||
| Email address | user1@address.invalid |
|
||||
| Email (again) | user1@address.invalid |
|
||||
| Email address | user1@example.com |
|
||||
| Email (again) | user1@example.com |
|
||||
| First name | User1 |
|
||||
| Last name | L1 |
|
||||
And I press "Create my new account"
|
||||
And I should see "Confirm your account"
|
||||
And I should see "An email should have been sent to your address at user1@address.invalid"
|
||||
And I should see "An email should have been sent to your address at user1@example.com"
|
||||
And I confirm email for "user1"
|
||||
And I should see "Thanks, User1 L1"
|
||||
And I should see "Your registration has been confirmed"
|
||||
@@ -244,19 +244,19 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I press "Next"
|
||||
And I should see "This privacy policy"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I should see "This site policy"
|
||||
And I should see "This privacy policy"
|
||||
# Confirm that a notification is displayed if none of the policies are accepted.
|
||||
When I set the field "I agree to the This site policy" to "0"
|
||||
And I set the field "I agree to the This privacy policy" to "0"
|
||||
When I set the field "I agree to the This site policy." to "0"
|
||||
And I set the field "I agree to the This privacy policy." to "0"
|
||||
And I press "Next"
|
||||
Then I should see "Please agree to the following policies"
|
||||
Then I should see "Please agree to the following policies:"
|
||||
And I should see "Before continuing you need to acknowledge all these policies."
|
||||
# Confirm that a notification is displayed if only some policies are accepted.
|
||||
When I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "0"
|
||||
Then I should see "Please agree to the following policies"
|
||||
When I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "0"
|
||||
Then I should see "Please agree to the following policies:"
|
||||
And I should see "Before continuing you need to acknowledge all these policies."
|
||||
|
||||
Scenario: Accept policy on login, do not accept all policies
|
||||
@@ -276,23 +276,23 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I press "Next"
|
||||
And I should see "This privacy policy"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I should see "This site policy"
|
||||
And I should see "This privacy policy"
|
||||
# Confirm that a notification is displayed if none of the policies are accepted.
|
||||
When I set the field "I agree to the This site policy" to "0"
|
||||
And I set the field "I agree to the This privacy policy" to "0"
|
||||
When I set the field "I agree to the This site policy." to "0"
|
||||
And I set the field "I agree to the This privacy policy." to "0"
|
||||
And I press "Next"
|
||||
Then I should see "Please agree to the following policies"
|
||||
Then I should see "Please agree to the following policies:"
|
||||
And I should see "Before continuing you need to acknowledge all these policies."
|
||||
# Confirm that a notification is displayed if only some policies are accepted.
|
||||
When I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "0"
|
||||
Then I should see "Please agree to the following policies"
|
||||
When I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "0"
|
||||
Then I should see "Please agree to the following policies:"
|
||||
And I should see "Before continuing you need to acknowledge all these policies."
|
||||
# Confirm that user can not browse the site (edit their profile).
|
||||
When I follow "Profile" in the user menu
|
||||
Then I should see "Please agree to the following policies"
|
||||
Then I should see "Please agree to the following policies:"
|
||||
|
||||
Scenario: Accept policy on login, accept all policies
|
||||
Given the following config values are set as admin:
|
||||
@@ -311,14 +311,14 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I press "Next"
|
||||
And I should see "This privacy policy"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I should see "This site policy"
|
||||
And I should see "This privacy policy"
|
||||
# User accepts all policies.
|
||||
When I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
When I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "1"
|
||||
And I press "Next"
|
||||
Then I should not see "Please agree to the following policies"
|
||||
Then I should not see "Please agree to the following policies:"
|
||||
And I should not see "Before continuing you need to acknowledge all these policies."
|
||||
# Confirm that user can login and browse the site (edit their profile).
|
||||
When I open my profile in edit mode
|
||||
@@ -328,7 +328,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
When I log in as "user1"
|
||||
Then I should not see "This site policy"
|
||||
And I should not see "This privacy policy"
|
||||
And I should not see "Please agree to the following policies"
|
||||
And I should not see "Please agree to the following policies:"
|
||||
# Confirm that user can login and browse the site (edit their profile).
|
||||
When I open my profile in edit mode
|
||||
Then the field "First name" matches value "User"
|
||||
@@ -350,14 +350,14 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I press "Next"
|
||||
And I should see "This privacy policy"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I should see "This site policy"
|
||||
And I should see "This privacy policy"
|
||||
# User accepts all policies.
|
||||
When I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
When I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "1"
|
||||
And I press "Next"
|
||||
Then I should not see "Please agree to the following policies"
|
||||
Then I should not see "Please agree to the following policies:"
|
||||
# Confirm that user can login and browse the site (edit their profile).
|
||||
When I open my profile in edit mode
|
||||
Then the field "First name" matches value "User"
|
||||
@@ -385,7 +385,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should not see "This privacy policy"
|
||||
Then I should see "This third parties policy"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I should see "This third parties policy"
|
||||
And I set the field "This third parties policy" to "1"
|
||||
And I press "Next"
|
||||
@@ -407,12 +407,12 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I log in as "user1"
|
||||
And I should see "This site policy"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I should see "This site policy"
|
||||
# User accepts policy.
|
||||
When I set the field "I agree to the This site policy" to "1"
|
||||
When I set the field "I agree to the This site policy." to "1"
|
||||
And I press "Next"
|
||||
Then I should not see "Please agree to the following policies"
|
||||
Then I should not see "Please agree to the following policies:"
|
||||
# Confirm that user can login and browse the site (edit their profile).
|
||||
When I open my profile in edit mode
|
||||
Then the field "First name" matches value "User"
|
||||
@@ -437,13 +437,13 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should see "short text2 new version"
|
||||
And I should see "full text2 new version"
|
||||
When I press "Next"
|
||||
Then I should see "Please agree to the following policies"
|
||||
Then I should see "Please agree to the following policies:"
|
||||
And I should see "This site policy new version"
|
||||
And I should see "short text2 new version"
|
||||
# User accepts policy.
|
||||
And I set the field "I agree to the This site policy new version" to "1"
|
||||
And I set the field "I agree to the This site policy new version." to "1"
|
||||
When I press "Next"
|
||||
Then I should not see "Please agree to the following policies"
|
||||
Then I should not see "Please agree to the following policies:"
|
||||
# Confirm that user can login and browse the site (edit their profile).
|
||||
When I open my profile in edit mode
|
||||
Then the field "First name" matches value "User"
|
||||
@@ -504,28 +504,28 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should see "short text3"
|
||||
And I should see "full text3"
|
||||
When I press "Next"
|
||||
Then I should see "Please agree to the following policies"
|
||||
Then I should see "Please agree to the following policies:"
|
||||
And I should see "This site policy" in the "region-main" "region"
|
||||
And I should see "short text2"
|
||||
And I should see "This privacy policy" in the "region-main" "region"
|
||||
And I should see "short text3"
|
||||
And I should not see "This guests policy" in the "region-main" "region"
|
||||
And I should not see "short text4"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "1"
|
||||
When I press "Next"
|
||||
Then I should not see "I understand and agree"
|
||||
And I should see "New account"
|
||||
And I set the following fields to these values:
|
||||
| Username | user1 |
|
||||
| Password | user1 |
|
||||
| Email address | user1@address.invalid |
|
||||
| Email (again) | user1@address.invalid |
|
||||
| Email address | user1@example.com |
|
||||
| Email (again) | user1@example.com |
|
||||
| First name | User1 |
|
||||
| Last name | L1 |
|
||||
When I press "Create my new account"
|
||||
Then I should see "Confirm your account"
|
||||
And I should see "An email should have been sent to your address at user1@address.invalid"
|
||||
And I should see "An email should have been sent to your address at user1@example.com"
|
||||
And I follow "Log in"
|
||||
When I click on "Create new account" "link"
|
||||
# Confirm that the user can view and accept policies when attempting to create another account.
|
||||
@@ -537,7 +537,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should see "short text3"
|
||||
And I should see "full text3"
|
||||
When I press "Next"
|
||||
Then I should see "Please agree to the following policies"
|
||||
Then I should see "Please agree to the following policies:"
|
||||
And I should see "This site policy" in the "region-main" "region"
|
||||
And I should see "short text2"
|
||||
And I should not see "full text2"
|
||||
@@ -547,8 +547,8 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should not see "This guests policy" in the "region-main" "region"
|
||||
And I should not see "short text4"
|
||||
And I should not see "full text4"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "1"
|
||||
When I press "Next"
|
||||
Then I should not see "I understand and agree"
|
||||
And I should see "New account"
|
||||
@@ -572,8 +572,8 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should see "Please read our This privacy policy"
|
||||
And I press "Next"
|
||||
And I should see "Viewing this page on behalf of User 1"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "1"
|
||||
And I press "Next"
|
||||
And I log out
|
||||
And I log in as "user1"
|
||||
@@ -599,15 +599,15 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
When I log in as "manager"
|
||||
And I press "Next"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "1"
|
||||
And I press "Next"
|
||||
And I am on the "user1" "user > profile" page
|
||||
And I follow "Log in as"
|
||||
Then I should see "You are logged in as User 1"
|
||||
And I press "Continue"
|
||||
And I should see "Policies and agreements"
|
||||
And I should see "No permission to agree to the policies on behalf of this user"
|
||||
And I should see "No permission to agree to the policies on behalf of this user."
|
||||
And I should see "Sorry, you do not have the required permission to agree to the following policies on behalf of User 1"
|
||||
|
||||
Scenario: Accept policy on sign up as a guest, one policy
|
||||
@@ -631,23 +631,23 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should see "short text2"
|
||||
And I should see "full text2"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I should see "This site policy"
|
||||
And I should see "short text2"
|
||||
And I should not see "full text2"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I press "Next"
|
||||
And I should not see "I understand and agree"
|
||||
And I set the following fields to these values:
|
||||
| Username | user1 |
|
||||
| Password | user1 |
|
||||
| Email address | user1@address.invalid |
|
||||
| Email (again) | user1@address.invalid |
|
||||
| Email address | user1@example.com |
|
||||
| Email (again) | user1@example.com |
|
||||
| First name | User1 |
|
||||
| Last name | L1 |
|
||||
And I press "Create my new account"
|
||||
And I should see "Confirm your account"
|
||||
And I should see "An email should have been sent to your address at user1@address.invalid"
|
||||
And I should see "An email should have been sent to your address at user1@example.com"
|
||||
And I confirm email for "user1"
|
||||
And I should see "Thanks, User1 L1"
|
||||
And I should see "Your registration has been confirmed"
|
||||
@@ -680,12 +680,12 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
Then I should see "Digital maturity declaration" in the "region-main" "region"
|
||||
And I should see "You declare be old enough"
|
||||
And I should see "Here goes content."
|
||||
And I press "I agree to the Digital maturity declaration"
|
||||
And I press "I agree to the Digital maturity declaration."
|
||||
# The second policy with the agreement style "on its own page" must be accepted now.
|
||||
And I should see "Terms of Service" in the "region-main" "region"
|
||||
And I should see "We teach, you learn"
|
||||
And I should see "Here goes content."
|
||||
And I press "I agree to the Terms of Service"
|
||||
And I press "I agree to the Terms of Service."
|
||||
# Only now we see the remaining consent page policies.
|
||||
And I should see "Policy 1 out of 2"
|
||||
And I should see "Privacy policy" in the "region-main" "region"
|
||||
@@ -697,26 +697,26 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should see "We eat cookies, srsly"
|
||||
And I should see "Here goes content."
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I should see "Privacy policy"
|
||||
And I should see "Cookies policy"
|
||||
And I should not see "Digital maturity declaration" in the "region-main" "region"
|
||||
And I should not see "Terms of Service" in the "region-main" "region"
|
||||
And I should not see "Here goes content."
|
||||
And I set the field "I agree to the Privacy policy" to "1"
|
||||
And I set the field "I agree to the Cookies policy" to "1"
|
||||
And I set the field "I agree to the Privacy policy." to "1"
|
||||
And I set the field "I agree to the Cookies policy." to "1"
|
||||
And I press "Next"
|
||||
And I should see "New account"
|
||||
And I set the following fields to these values:
|
||||
| Username | user1 |
|
||||
| Password | user1 |
|
||||
| Email address | user1@address.invalid |
|
||||
| Email (again) | user1@address.invalid |
|
||||
| Email address | user1@example.com |
|
||||
| Email (again) | user1@example.com |
|
||||
| First name | User1 |
|
||||
| Last name | L1 |
|
||||
And I press "Create my new account"
|
||||
And I should see "Confirm your account"
|
||||
And I should see "An email should have been sent to your address at user1@address.invalid"
|
||||
And I should see "An email should have been sent to your address at user1@example.com"
|
||||
And I confirm email for "user1"
|
||||
And I should see "Thanks, User1 L1"
|
||||
And I should see "Your registration has been confirmed"
|
||||
@@ -751,7 +751,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
Then I should see "Digital maturity declaration" in the "region-main" "region"
|
||||
And I should see "You declare be old enough"
|
||||
And I should see "Here goes content."
|
||||
And I press "I agree to the Digital maturity declaration"
|
||||
And I press "I agree to the Digital maturity declaration."
|
||||
# The second policy with the agreement style "on its own page" must be accepted now.
|
||||
And I should see "Terms of Service" in the "region-main" "region"
|
||||
And I should see "We teach, you learn"
|
||||
@@ -762,7 +762,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should see "Terms of Service" in the "region-main" "region"
|
||||
And I should see "We teach, you learn"
|
||||
And I should see "Here goes content."
|
||||
And I press "I agree to the Terms of Service"
|
||||
And I press "I agree to the Terms of Service."
|
||||
# Only now we see the remaining consent page policies.
|
||||
And I should see "Policy 1 out of 2"
|
||||
And I should see "Privacy policy" in the "region-main" "region"
|
||||
@@ -774,14 +774,14 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should see "We eat cookies, srsly"
|
||||
And I should see "Here goes content."
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I should see "Privacy policy"
|
||||
And I should see "Cookies policy"
|
||||
And I should not see "Digital maturity declaration" in the "region-main" "region"
|
||||
And I should not see "Terms of Service" in the "region-main" "region"
|
||||
And I should not see "Here goes content."
|
||||
And I set the field "I agree to the Privacy policy" to "1"
|
||||
And I set the field "I agree to the Cookies policy" to "1"
|
||||
And I set the field "I agree to the Privacy policy." to "1"
|
||||
And I set the field "I agree to the Cookies policy." to "1"
|
||||
And I press "Next"
|
||||
And I follow "Profile" in the user menu
|
||||
# User can see his own agreements in the profile.
|
||||
@@ -809,15 +809,15 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
Then I should see "Privacy policy" in the "region-main" "region"
|
||||
And I should see "We scan your thoughts"
|
||||
And I should see "Here goes content."
|
||||
And I press "I agree to the Privacy policy"
|
||||
And I press "I agree to the Privacy policy."
|
||||
And I should see "Digital maturity declaration" in the "region-main" "region"
|
||||
And I should see "You declare be old enough"
|
||||
And I should see "Here goes content."
|
||||
And I press "I agree to the Digital maturity declaration"
|
||||
And I press "I agree to the Digital maturity declaration."
|
||||
And I should see "Terms of Service" in the "region-main" "region"
|
||||
And I should see "We teach, you learn"
|
||||
And I should see "Here goes content."
|
||||
And I press "I agree to the Terms of Service"
|
||||
And I press "I agree to the Terms of Service."
|
||||
And I follow "Profile" in the user menu
|
||||
And I follow "Policies and agreements"
|
||||
And "Accepted" "text" should exist in the "Privacy policy" "table_row"
|
||||
@@ -844,21 +844,21 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
Then I should see "Digital maturity declaration" in the "region-main" "region"
|
||||
And I should see "You declare be old enough"
|
||||
And I should see "Here goes content."
|
||||
And I press "I agree to the Digital maturity declaration"
|
||||
And I press "I agree to the Digital maturity declaration."
|
||||
And I should see "Cookies policy" in the "region-main" "region"
|
||||
And I should see "We eat cookies, srsly"
|
||||
And I press "I agree to the Cookies policy"
|
||||
And I press "I agree to the Cookies policy."
|
||||
And I should see "New account"
|
||||
And I set the following fields to these values:
|
||||
| Username | user1 |
|
||||
| Password | user1 |
|
||||
| Email address | user1@address.invalid |
|
||||
| Email (again) | user1@address.invalid |
|
||||
| Email address | user1@example.com |
|
||||
| Email (again) | user1@example.com |
|
||||
| First name | User1 |
|
||||
| Last name | L1 |
|
||||
And I press "Create my new account"
|
||||
And I should see "Confirm your account"
|
||||
And I should see "An email should have been sent to your address at user1@address.invalid"
|
||||
And I should see "An email should have been sent to your address at user1@example.com"
|
||||
And I confirm email for "user1"
|
||||
And I should see "Thanks, User1 L1"
|
||||
And I should see "Your registration has been confirmed"
|
||||
|
||||
@@ -61,7 +61,7 @@ Feature: Manage policies
|
||||
| Policy1 | v1 | full text2 | short text2 | active |
|
||||
And I log in as "manager"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the Policy1" to "1"
|
||||
And I set the field "I agree to the Policy1." to "1"
|
||||
And I press "Next"
|
||||
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
|
||||
And I click on "Edit" "link" in the "Policy1" "table_row"
|
||||
@@ -83,7 +83,7 @@ Feature: Manage policies
|
||||
| Policy1 | v1 | full text2 | short text2 | active |
|
||||
And I log in as "manager"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the Policy1" to "1"
|
||||
And I set the field "I agree to the Policy1." to "1"
|
||||
And I press "Next"
|
||||
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
|
||||
And I click on "Edit" "link" in the "Policy1" "table_row"
|
||||
@@ -101,7 +101,7 @@ Feature: Manage policies
|
||||
| Policy1 | v1 | full text2 | short text2 | active |
|
||||
And I log in as "manager"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the Policy1" to "1"
|
||||
And I set the field "I agree to the Policy1." to "1"
|
||||
And I press "Next"
|
||||
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
|
||||
And I click on "Edit" "link" in the "Policy1" "table_row"
|
||||
@@ -180,7 +180,7 @@ Feature: Manage policies
|
||||
| Policy1 | v1 | full text2 | short text2 | active |
|
||||
And I log in as "manager"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the Policy1" to "1"
|
||||
And I set the field "I agree to the Policy1." to "1"
|
||||
And I press "Next"
|
||||
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
|
||||
And I click on "Set status to \"Inactive\"" "link" in the "Policy1" "table_row"
|
||||
@@ -218,7 +218,7 @@ Feature: Manage policies
|
||||
| Policy1 | v1 | full text2 | short text2 | active |
|
||||
And I log in as "manager"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the Policy1" to "1"
|
||||
And I set the field "I agree to the Policy1." to "1"
|
||||
And I press "Next"
|
||||
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
|
||||
And I click on "Set status to \"Inactive\"" "link" in the "Policy1" "table_row"
|
||||
@@ -247,7 +247,7 @@ Feature: Manage policies
|
||||
| Policy1 | v1 | full text2 | short text2 | active |
|
||||
And I log in as "user1"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the Policy1" to "1"
|
||||
And I set the field "I agree to the Policy1." to "1"
|
||||
And I press "Next"
|
||||
And I follow "Preferences" in the user menu
|
||||
And I should see "Preferences"
|
||||
|
||||
@@ -70,26 +70,26 @@ Feature: Optional policies
|
||||
And I should see "OwnPageCompulsory1" in the "region-main" "region"
|
||||
And I should see "short text4" in the "region-main" "region"
|
||||
And I should see "full text4" in the "region-main" "region"
|
||||
And I press "I agree to the OwnPageCompulsory1"
|
||||
And I press "I agree to the OwnPageCompulsory1."
|
||||
# Compulsory policies displayed on the consent page are shown next and must be agreed.
|
||||
And I should see "ConsentPageCompulsory1"
|
||||
And I should see "short text3" in the "region-main" "region"
|
||||
And I should see "full text3" in the "region-main" "region"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I set the field "I agree to the ConsentPageCompulsory1" to "1"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I set the field "I agree to the ConsentPageCompulsory1." to "1"
|
||||
And I press "Next"
|
||||
# The signup form can be submitted and a new account created.
|
||||
And I set the following fields to these values:
|
||||
| Username | user3 |
|
||||
| Password | user3 |
|
||||
| Email address | user3@address.invalid |
|
||||
| Email (again) | user3@address.invalid |
|
||||
| Email address | user3@example.com |
|
||||
| Email (again) | user3@example.com |
|
||||
| First name | User3 |
|
||||
| Last name | L3 |
|
||||
And I press "Create my new account"
|
||||
And I should see "Confirm your account"
|
||||
And I should see "An email should have been sent to your address at user3@address.invalid"
|
||||
And I should see "An email should have been sent to your address at user3@example.com"
|
||||
And I confirm email for "user3"
|
||||
And I should see "Thanks, User3 L3"
|
||||
And I should see "Your registration has been confirmed"
|
||||
@@ -99,7 +99,7 @@ Feature: Optional policies
|
||||
Then I should see "OwnPageOptional1"
|
||||
And I should see "short text5" in the "region-main" "region"
|
||||
And I should see "full text5" in the "region-main" "region"
|
||||
And I press "No thanks, I decline OwnPageOptional1"
|
||||
And I press "I don't agree to the OwnPageOptional1"
|
||||
# Then come policies displayed on the consent page.
|
||||
And I should see "ConsentPageOptional1" in the "region-main" "region"
|
||||
And I should see "short text1" in the "region-main" "region"
|
||||
@@ -109,9 +109,9 @@ Feature: Optional policies
|
||||
And I should see "short text2" in the "region-main" "region"
|
||||
And I should see "full text2" in the "region-main" "region"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I set the field "I agree to the ConsentPageOptional1" to "1"
|
||||
And I set the field "No thanks, I decline ConsentPageOptional2" to "0"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I set the field "I agree to the ConsentPageOptional1." to "1"
|
||||
And I set the field "I don't agree to the ConsentPageOptional2" to "0"
|
||||
And I press "Next"
|
||||
# Accepted and declined policies are shown in the profile.
|
||||
And I follow "Profile" in the user menu
|
||||
@@ -132,14 +132,14 @@ Feature: Optional policies
|
||||
Then I should see "OwnPageOptional1"
|
||||
And I should see "short text5" in the "region-main" "region"
|
||||
And I should see "full text5" in the "region-main" "region"
|
||||
And I press "I agree to the OwnPageOptional1"
|
||||
And I press "I agree to the OwnPageOptional1."
|
||||
# Then come policies displayed on the consent page.
|
||||
And I should see "ConsentPageOptional1" in the "region-main" "region"
|
||||
And I should see "short text1" in the "region-main" "region"
|
||||
And I should see "full text1" in the "region-main" "region"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I set the field "No thanks, I decline ConsentPageOptional1" to "0"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I set the field "I don't agree to the ConsentPageOptional1" to "0"
|
||||
And I press "Next"
|
||||
# Accepted and declined policies are shown in the profile.
|
||||
And I follow "Profile" in the user menu
|
||||
@@ -152,7 +152,7 @@ Feature: Optional policies
|
||||
| Name | Content | Summary | Agreementstyle | Optional |
|
||||
| OwnPageOptional1 | full text1 | short text1 | 1 | 1 |
|
||||
And I log in as "user1"
|
||||
And I press "I agree to the OwnPageOptional1"
|
||||
And I press "I agree to the OwnPageOptional1."
|
||||
And I follow "Profile" in the user menu
|
||||
And I follow "Policies and agreements"
|
||||
And "Accepted" "text" should exist in the "OwnPageOptional1" "table_row"
|
||||
@@ -177,7 +177,7 @@ Feature: Optional policies
|
||||
| Name | Content | Summary | Agreementstyle | Optional |
|
||||
| OwnPageOptional1 | full text1 | short text1 | 1 | 1 |
|
||||
And I log in as "user1"
|
||||
And I press "I agree to the OwnPageOptional1"
|
||||
And I press "I agree to the OwnPageOptional1."
|
||||
And I follow "Profile" in the user menu
|
||||
And I follow "Policies and agreements"
|
||||
And "Accepted" "text" should exist in the "OwnPageOptional1" "table_row"
|
||||
@@ -202,12 +202,12 @@ Feature: Optional policies
|
||||
| OwnPageOptional1 | full text1 | short text1 | 1 | 1 |
|
||||
| OwnPageOptional2 | full text2 | short text2 | 1 | 1 |
|
||||
And I log in as "user1"
|
||||
And I press "I agree to the OwnPageOptional1"
|
||||
And I press "No thanks, I decline OwnPageOptional2"
|
||||
And I press "I agree to the OwnPageOptional1."
|
||||
And I press "I don't agree to the OwnPageOptional2"
|
||||
And I log out
|
||||
And I log in as "manager"
|
||||
And I press "I agree to the OwnPageOptional1"
|
||||
And I press "I agree to the OwnPageOptional2"
|
||||
And I press "I agree to the OwnPageOptional1."
|
||||
And I press "I agree to the OwnPageOptional2."
|
||||
When I navigate to "Users > Privacy and policies > User agreements" in site administration
|
||||
# User One has accepted just some policies.
|
||||
Then "Partially accepted" "text" should exist in the "User One" "table_row"
|
||||
@@ -228,8 +228,8 @@ Feature: Optional policies
|
||||
| OwnPageOptional1 | full text1 | short text1 | 1 | 1 |
|
||||
| OwnPageOptional2 | full text2 | short text2 | 1 | 1 |
|
||||
And I log in as "user1"
|
||||
And I press "I agree to the OwnPageOptional1"
|
||||
And I press "No thanks, I decline OwnPageOptional2"
|
||||
And I press "I agree to the OwnPageOptional1."
|
||||
And I press "I don't agree to the OwnPageOptional2"
|
||||
And I log out
|
||||
And I log in as "admin"
|
||||
When I navigate to "Users > Privacy and policies > User agreements" in site administration
|
||||
|
||||
@@ -178,12 +178,7 @@ class tool_uploadcourse_helper {
|
||||
* @return enrol_plugin[]
|
||||
*/
|
||||
public static function get_enrolment_plugins() {
|
||||
$cache = cache::make('tool_uploadcourse', 'helper');
|
||||
if (($enrol = $cache->get('enrol')) === false) {
|
||||
$enrol = enrol_get_plugins(false);
|
||||
$cache->set('enrol', $enrol);
|
||||
}
|
||||
return $enrol;
|
||||
return enrol_get_plugins(false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -208,12 +208,11 @@ class helper {
|
||||
* @return \moodle_url The URL.
|
||||
*/
|
||||
public static function get_duplicate_tour_link($tourid) {
|
||||
$link = new \moodle_url('/admin/tool/usertours/configure.php', [
|
||||
return new \moodle_url('/admin/tool/usertours/configure.php', [
|
||||
'action' => manager::ACTION_DUPLICATETOUR,
|
||||
'id' => $tourid,
|
||||
'sesskey' => sesskey(),
|
||||
]);
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -503,8 +503,9 @@ class manager {
|
||||
* @param int $tourid The ID of the tour to duplicate.
|
||||
*/
|
||||
protected function duplicate_tour($tourid) {
|
||||
$tour = helper::get_tour($tourid);
|
||||
require_sesskey();
|
||||
|
||||
$tour = helper::get_tour($tourid);
|
||||
$export = $tour->to_record();
|
||||
// Remove the id.
|
||||
unset($export->id);
|
||||
|
||||
@@ -102,7 +102,6 @@ $string['name'] = 'Name';
|
||||
$string['name_help'] = 'The name of a tour may be added as plain text, enclosed in multilang tags (for use with the multi-language content filter) if required.
|
||||
|
||||
Alternatively, a language string ID may be entered in the format identifier,component (with no brackets or space after the comma).';
|
||||
$string['newstep'] = 'Create step';
|
||||
$string['newstep'] = 'New step';
|
||||
$string['newtour'] = 'Create a new tour';
|
||||
$string['next'] = 'Next';
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
@tool @tool_xmldb
|
||||
Feature: The Reconcile XMLDB files report works and all the files are ok
|
||||
In order to ensure that all the XMLDB Editor xml files are generated properly
|
||||
As a developer
|
||||
I need to be able to run the Reconcile XMLDB files report and verify that everything is ok
|
||||
|
||||
Scenario: The Reconcile XMLDB files reports that all files are ok
|
||||
Given I log in as "admin"
|
||||
And I navigate to "Development > XMLDB editor" in site administration
|
||||
When I follow "[Reconcile XMLDB files]"
|
||||
Then I should see "Look for XMLDB files needing reconciling"
|
||||
And I should see "All files are OK. No reconciling is needed."
|
||||
@@ -1925,6 +1925,7 @@ class model {
|
||||
$uniqueid = $this->get_time_splitting()->append_rangeindex($dbprediction->sampleid, $dbprediction->rangeindex);
|
||||
$predictionrecords[$uniqueid]->id = $dbprediction->id;
|
||||
}
|
||||
$dbpredictions->close();
|
||||
}
|
||||
|
||||
return $predictionrecords;
|
||||
|
||||
@@ -370,7 +370,7 @@ class core_auth_external extends external_api {
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public static function resend_confirmation_email($username, $password, $redirect = '') {
|
||||
global $PAGE;
|
||||
global $PAGE, $CFG;
|
||||
|
||||
$warnings = array();
|
||||
$params = self::validate_parameters(
|
||||
@@ -387,20 +387,27 @@ class core_auth_external extends external_api {
|
||||
$username = trim(core_text::strtolower($params['username']));
|
||||
$password = $params['password'];
|
||||
|
||||
$user = core_user::get_user_by_username($username);
|
||||
|
||||
if (!empty($user) && $user->confirmed) {
|
||||
if (!empty($CFG->protectusernames)) {
|
||||
throw new moodle_exception('invalidlogin');
|
||||
}
|
||||
throw new moodle_exception('alreadyconfirmed');
|
||||
}
|
||||
|
||||
if (is_restored_user($username)) {
|
||||
if (!empty($CFG->protectusernames)) {
|
||||
throw new moodle_exception('invalidlogin');
|
||||
}
|
||||
throw new moodle_exception('restoredaccountresetpassword', 'webservice');
|
||||
}
|
||||
|
||||
$user = authenticate_user_login($username, $password);
|
||||
|
||||
if (empty($user)) {
|
||||
throw new moodle_exception('invalidlogin');
|
||||
}
|
||||
|
||||
if ($user->confirmed) {
|
||||
throw new moodle_exception('alreadyconfirmed');
|
||||
}
|
||||
|
||||
// Check if we should redirect the user once the user is confirmed.
|
||||
$confirmationurl = null;
|
||||
if (!empty($params['redirect'])) {
|
||||
|
||||
@@ -15,13 +15,13 @@ Feature: User must accept policy when logging in and signing up
|
||||
And I set the following fields to these values:
|
||||
| Username | user1 |
|
||||
| Password | user1 |
|
||||
| Email address | user1@address.invalid |
|
||||
| Email (again) | user1@address.invalid |
|
||||
| Email address | student1@example.com |
|
||||
| Email (again) | student1@example.com |
|
||||
| First name | User1 |
|
||||
| Last name | L1 |
|
||||
And I press "Create my new account"
|
||||
And I should see "Confirm your account"
|
||||
And I should see "An email should have been sent to your address at user1@address.invalid"
|
||||
And I should see "An email should have been sent to your address at student1@example.com"
|
||||
And I confirm email for "user1"
|
||||
And I should see "Thanks, User1 L1"
|
||||
And I should see "Your registration has been confirmed"
|
||||
@@ -45,14 +45,14 @@ Feature: User must accept policy when logging in and signing up
|
||||
And I set the following fields to these values:
|
||||
| Username | user1 |
|
||||
| Password | user1 |
|
||||
| Email address | user1@address.invalid |
|
||||
| Email (again) | user1@address.invalid |
|
||||
| Email address | user1@example.com |
|
||||
| Email (again) | user1@example.com |
|
||||
| First name | User1 |
|
||||
| Last name | L1 |
|
||||
| I understand and agree | 1 |
|
||||
And I press "Create my new account"
|
||||
And I should see "Confirm your account"
|
||||
And I should see "An email should have been sent to your address at user1@address.invalid"
|
||||
And I should see "An email should have been sent to your address at user1@example.com"
|
||||
And I confirm email for "user1"
|
||||
And I should see "Thanks, User1 L1"
|
||||
And I should see "Your registration has been confirmed"
|
||||
@@ -67,8 +67,8 @@ Feature: User must accept policy when logging in and signing up
|
||||
Scenario Outline: Email validation during email registration
|
||||
Given the following config values are set as admin:
|
||||
| allowaccountssameemail | <allowsameemail> |
|
||||
| registerauth | email |
|
||||
| passwordpolicy | 0 |
|
||||
| registerauth | email |
|
||||
| passwordpolicy | 0 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| s1 | John | Doe | s1@example.com |
|
||||
|
||||
@@ -104,6 +104,7 @@ class auth_plugin_lti extends \auth_plugin_base {
|
||||
* @param int $provisioningmode the desired account provisioning mode, which controls the auth flow for unbound users.
|
||||
* @param array $legacyconsumersecrets an array of secrets used by the legacy consumer if a migration claim exists.
|
||||
* @throws coding_exception if the specified provisioning mode is invalid.
|
||||
* @throws moodle_exception if user authentication fails.
|
||||
*/
|
||||
public function complete_login(array $launchdata, moodle_url $returnurl, int $provisioningmode,
|
||||
array $legacyconsumersecrets = []): void {
|
||||
@@ -112,6 +113,19 @@ class auth_plugin_lti extends \auth_plugin_base {
|
||||
if ($this->get_user_binding($launchdata['iss'], $launchdata['sub'])) {
|
||||
$user = $this->find_or_create_user_from_launch($launchdata);
|
||||
|
||||
if ($user->suspended) {
|
||||
$failurereason = AUTH_LOGIN_SUSPENDED;
|
||||
$event = \core\event\user_login_failed::create([
|
||||
'userid' => $user->id,
|
||||
'other' => [
|
||||
'username' => $user->username,
|
||||
'reason' => $failurereason
|
||||
]
|
||||
]);
|
||||
$event->trigger();
|
||||
throw new moodle_exception('invalidlogin', 'core');
|
||||
}
|
||||
|
||||
if (isloggedin()) {
|
||||
// If a different user is currently logged in, authenticate the linked user instead.
|
||||
global $USER;
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
require_once('../../config.php');
|
||||
|
||||
$issuerid = required_param('id', PARAM_INT);
|
||||
$wantsurl = new moodle_url(optional_param('wantsurl', '', PARAM_URL));
|
||||
$wantsurl = new moodle_url(optional_param('wantsurl', '', PARAM_LOCALURL));
|
||||
|
||||
$PAGE->set_context(context_system::instance());
|
||||
$PAGE->set_url(new moodle_url('/auth/oauth2/login.php', ['id' => $issuerid]));
|
||||
|
||||
+9
@@ -181,6 +181,7 @@ final class external_test extends externallib_advanced_testcase {
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertEmpty($result['warnings']);
|
||||
|
||||
set_config('protectusernames', 0);
|
||||
$_SERVER['HTTP_USER_AGENT'] = 'no browser'; // Hack around missing user agent in CLI scripts.
|
||||
$this->expectException('\moodle_exception');
|
||||
$this->expectExceptionMessage('error/invalidlogin');
|
||||
@@ -204,6 +205,7 @@ final class external_test extends externallib_advanced_testcase {
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertEmpty($result['warnings']);
|
||||
|
||||
set_config('protectusernames', 0);
|
||||
$_SERVER['HTTP_USER_AGENT'] = 'no browser'; // Hack around missing user agent in CLI scripts.
|
||||
$this->expectException('\moodle_exception');
|
||||
$this->expectExceptionMessage('error/invalidlogin');
|
||||
@@ -234,6 +236,13 @@ final class external_test extends externallib_advanced_testcase {
|
||||
$result = external_api::clean_returnvalue(core_auth_external::confirm_user_returns(), $result);
|
||||
$this->assertTrue($result['success']);
|
||||
|
||||
// Keep protectusernames enabled so the call returns invalidlogin exception.
|
||||
$this->expectException('\moodle_exception');
|
||||
$this->expectExceptionMessage('error/invalidlogin');
|
||||
core_auth_external::resend_confirmation_email($username, $password);
|
||||
|
||||
// Now disable protectusernames and expect an exception.
|
||||
set_config('protectusernames', 0);
|
||||
$this->expectException('\moodle_exception');
|
||||
$this->expectExceptionMessage('error/alreadyconfirmed');
|
||||
core_auth_external::resend_confirmation_email($username, $password);
|
||||
|
||||
@@ -73,7 +73,15 @@ class auth_plugin_webservice extends auth_plugin_base {
|
||||
function user_login_webservice($username, $password) {
|
||||
global $CFG, $DB;
|
||||
// special web service login
|
||||
if ($user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id))) {
|
||||
$user = $DB->get_record('user', [
|
||||
'username' => $username,
|
||||
'auth' => 'webservice',
|
||||
'deleted' => '0',
|
||||
'suspended' => '0',
|
||||
'mnethostid' => $CFG->mnet_localhost_id,
|
||||
]);
|
||||
|
||||
if ($user) {
|
||||
return validate_internal_user_password($user, $password);
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -117,6 +117,11 @@ abstract class frontend {
|
||||
foreach ($enabled as $plugin => $info) {
|
||||
// Create plugin front-end object.
|
||||
$class = '\availability_' . $plugin . '\frontend';
|
||||
if (!class_exists($class)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var \core_availability\frontend $frontend */
|
||||
$frontend = new $class();
|
||||
|
||||
// Add to array of required YUI modules.
|
||||
|
||||
@@ -792,11 +792,10 @@ abstract class info {
|
||||
// plugin could also rely on the completion plugin.
|
||||
$pluginmanager = \core_plugin_manager::instance();
|
||||
$enabled = $pluginmanager->get_enabled_plugins('availability');
|
||||
$componentparams = new \stdClass();
|
||||
foreach ($enabled as $plugin => $info) {
|
||||
// Use the static method.
|
||||
/** @var \core_availability\condition $class */
|
||||
$class = '\availability_' . $plugin . '\condition';
|
||||
if ($class::completion_value_used($course, $cmid)) {
|
||||
if (class_exists($class) && $class::completion_value_used($course, $cmid)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,9 +220,9 @@ class tree extends tree_node {
|
||||
|
||||
// First see if it's a condition. These have a defined type.
|
||||
if (isset($child->type)) {
|
||||
// Look for a plugin of this type.
|
||||
/** @var \core_availability\condition $classname */
|
||||
$classname = '\availability_' . $child->type . '\condition';
|
||||
if (!array_key_exists($child->type, $enabled)) {
|
||||
if (!array_key_exists($child->type, $enabled) || !class_exists($classname)) {
|
||||
if ($lax) {
|
||||
// On load of existing settings, ignore if class
|
||||
// doesn't exist.
|
||||
|
||||
@@ -47,6 +47,11 @@ abstract class restore_qtype_plugin extends restore_plugin {
|
||||
*/
|
||||
private $questionanswercacheid = null;
|
||||
|
||||
/**
|
||||
* @var array List of fields to exclude form hashing during restore.
|
||||
*/
|
||||
protected array $excludedhashfields = [];
|
||||
|
||||
/**
|
||||
* Add to $paths the restore_path_elements needed
|
||||
* to handle question_answers for a given question
|
||||
@@ -62,6 +67,10 @@ abstract class restore_qtype_plugin extends restore_plugin {
|
||||
$elename = 'question_answer';
|
||||
$elepath = $this->get_pathfor('/answers/answer'); // we used get_recommended_name() so this works
|
||||
$paths[] = new restore_path_element($elename, $elepath);
|
||||
$this->exclude_identity_hash_fields([
|
||||
'/options/answers/id',
|
||||
'/options/answers/question',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,6 +87,10 @@ abstract class restore_qtype_plugin extends restore_plugin {
|
||||
$elename = 'question_numerical_unit';
|
||||
$elepath = $this->get_pathfor('/numerical_units/numerical_unit'); // we used get_recommended_name() so this works
|
||||
$paths[] = new restore_path_element($elename, $elepath);
|
||||
$this->exclude_identity_hash_fields([
|
||||
'/options/units/id',
|
||||
'/options/units/question',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,6 +107,7 @@ abstract class restore_qtype_plugin extends restore_plugin {
|
||||
$elename = 'question_numerical_option';
|
||||
$elepath = $this->get_pathfor('/numerical_options/numerical_option'); // we used get_recommended_name() so this works
|
||||
$paths[] = new restore_path_element($elename, $elepath);
|
||||
$this->exclude_identity_hash_fields(['/options/question']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,6 +128,21 @@ abstract class restore_qtype_plugin extends restore_plugin {
|
||||
$elename = 'question_dataset_item';
|
||||
$elepath = $this->get_pathfor('/dataset_definitions/dataset_definition/dataset_items/dataset_item');
|
||||
$paths[] = new restore_path_element($elename, $elepath);
|
||||
$this->exclude_identity_hash_fields([
|
||||
'/options/datasets/id',
|
||||
'/options/datasets/question',
|
||||
'/options/datasets/category',
|
||||
'/options/datasets/type',
|
||||
'/options/datasets/items/id',
|
||||
// The following fields aren't included in the backup or DB structure, but are parsed from the options field.
|
||||
'/options/datasets/status',
|
||||
'/options/datasets/distribution',
|
||||
'/options/datasets/minimum',
|
||||
'/options/datasets/maximum',
|
||||
'/options/datasets/decimals',
|
||||
// This field is set dynamically from the count of items in the dataset, it is not backed up.
|
||||
'/options/datasets/number_of_items',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,8 +197,7 @@ abstract class restore_qtype_plugin extends restore_plugin {
|
||||
$this->questionanswercacheid = $newquestionid;
|
||||
// Cache all cleaned answers for a simple text match.
|
||||
foreach ($answers as $answer) {
|
||||
// MDL-30018: Clean in the same way as {@link xml_writer::xml_safe_utf8()}.
|
||||
$clean = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is','', $answer->answer); // Clean CTRL chars.
|
||||
$clean = core_text::trim_ctrl_chars($answer->answer); // Clean CTRL chars.
|
||||
$clean = preg_replace("/\r\n|\r/", "\n", $clean); // Normalize line ending.
|
||||
$this->questionanswercache[$clean] = $answer->id;
|
||||
}
|
||||
@@ -395,4 +423,183 @@ abstract class restore_qtype_plugin extends restore_plugin {
|
||||
|
||||
return $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add fields to the list of fields excluded from hashing.
|
||||
*
|
||||
* This allows common methods to add fields to the exclusion list.
|
||||
*
|
||||
* @param array $fields
|
||||
* @return void
|
||||
*/
|
||||
private function exclude_identity_hash_fields(array $fields): void {
|
||||
$this->excludedhashfields = array_merge($this->excludedhashfields, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return fields to be excluded from hashing during restores.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
final public function get_excluded_identity_hash_fields(): array {
|
||||
return array_unique(array_merge(
|
||||
$this->excludedhashfields,
|
||||
$this->define_excluded_identity_hash_fields(),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of paths to fields to be removed from questiondata before creating an identity hash.
|
||||
*
|
||||
* Fields that should be excluded from common elements such as answers or numerical units that are used by the plugin will
|
||||
* be excluded automatically. This method just needs to define any specific to this plugin, such as foreign keys used in the
|
||||
* plugin's tables.
|
||||
*
|
||||
* The returned array should be a list of slash-delimited paths to locate the fields to be removed from the questiondata object.
|
||||
* For example, if you want to remove the field `$questiondata->options->questionid`, the path would be '/options/questionid'.
|
||||
* If a field in the path is an array, the rest of the path will be applied to each object in the array. So if you have
|
||||
* `$questiondata->options->answers[]`, the path '/options/answers/id' will remove the 'id' field from each element of the
|
||||
* 'answers' array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function define_excluded_identity_hash_fields(): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the backup structure of this question type into a structure matching its question data
|
||||
*
|
||||
* This should take the hierarchical array of tags from the question's backup structure, and return a structure that matches
|
||||
* that returned when calling {@see get_question_options()} for this question type.
|
||||
* See https://docs.moodle.org/dev/Question_data_structures#Representation_1:_%24questiondata for an explanation of this
|
||||
* structure.
|
||||
*
|
||||
* This data will then be used to produce an identity hash for comparison with questions in the database.
|
||||
*
|
||||
* This base implementation deals with all common backup elements created by the add_question_*_options() methods in this class,
|
||||
* plus elements added by ::define_question_plugin_structure() named for the qtype. The question type will need to extend
|
||||
* this function if ::define_question_plugin_structure() adds any other elements to the backup.
|
||||
*
|
||||
* @param array $backupdata The hierarchical array of tags from the backup.
|
||||
* @return \stdClass The questiondata object.
|
||||
*/
|
||||
public static function convert_backup_to_questiondata(array $backupdata): \stdClass {
|
||||
// Create an object from the top-level fields.
|
||||
$questiondata = (object) array_filter($backupdata, fn($tag) => !is_array($tag));
|
||||
$qtype = $questiondata->qtype;
|
||||
$questiondata->options = new stdClass();
|
||||
if (isset($backupdata["plugin_qtype_{$qtype}_question"][$qtype])) {
|
||||
$questiondata->options = (object) $backupdata["plugin_qtype_{$qtype}_question"][$qtype][0];
|
||||
}
|
||||
if (isset($backupdata["plugin_qtype_{$qtype}_question"]['answers'])) {
|
||||
$questiondata->options->answers = array_map(
|
||||
fn($answer) => (object) $answer,
|
||||
$backupdata["plugin_qtype_{$qtype}_question"]['answers']['answer'],
|
||||
);
|
||||
}
|
||||
if (isset($backupdata["plugin_qtype_{$qtype}_question"]['numerical_options'])) {
|
||||
$questiondata->options = (object) array_merge(
|
||||
(array) $questiondata->options,
|
||||
$backupdata["plugin_qtype_{$qtype}_question"]['numerical_options']['numerical_option'][0],
|
||||
);
|
||||
}
|
||||
if (isset($backupdata["plugin_qtype_{$qtype}_question"]['numerical_units'])) {
|
||||
$questiondata->options->units = array_map(
|
||||
fn($unit) => (object) $unit,
|
||||
$backupdata["plugin_qtype_{$qtype}_question"]['numerical_units']['numerical_unit'],
|
||||
);
|
||||
}
|
||||
if (isset($backupdata["plugin_qtype_{$qtype}_question"]['dataset_definitions'])) {
|
||||
$questiondata->options->datasets = array_map(
|
||||
fn($dataset) => (object) $dataset,
|
||||
$backupdata["plugin_qtype_{$qtype}_question"]['dataset_definitions']['dataset_definition'],
|
||||
);
|
||||
}
|
||||
if (isset($questiondata->options->datasets)) {
|
||||
foreach ($questiondata->options->datasets as $dataset) {
|
||||
if (isset($dataset->dataset_items)) {
|
||||
$dataset->items = array_map(
|
||||
fn($item) => (object) $item,
|
||||
$dataset->dataset_items['dataset_item'],
|
||||
);
|
||||
unset($dataset->dataset_items);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isset($backupdata['question_hints'])) {
|
||||
$questiondata->hints = array_map(
|
||||
fn($hint) => (object) $hint,
|
||||
$backupdata['question_hints']['question_hint'],
|
||||
);
|
||||
}
|
||||
|
||||
return $questiondata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove excluded fields from the questiondata structure.
|
||||
*
|
||||
* This removes fields that will not match or not be present in the question data structure produced by
|
||||
* {@see self::convert_backup_to_questiondata()} and {@see get_question_options()} (such as IDs), so that the remaining data can
|
||||
* be used to produce an identity hash for comparing the two.
|
||||
*
|
||||
* For plugins, it should be sufficient to override {@see self::define_excluded_identity_hash_fields()} with a list of paths
|
||||
* specific to the plugin type. Overriding this method is only necessary if the plugin's
|
||||
* {@see question_type::get_question_options()} method adds additional data to the question that is not included in the backup.
|
||||
*
|
||||
* @param stdClass $questiondata
|
||||
* @param array $excludefields Paths to the fields to exclude.
|
||||
* @return stdClass The $questiondata with excluded fields removed.
|
||||
*/
|
||||
public static function remove_excluded_question_data(stdClass $questiondata, array $excludefields = []): stdClass {
|
||||
// All questions will need to exclude 'id' (used by question and other tables), 'questionid' (used by hints and options),
|
||||
// 'createdby' and 'modifiedby' (since they won't map between sites).
|
||||
$defaultexcludes = [
|
||||
'/id',
|
||||
'/createdby',
|
||||
'/modifiedby',
|
||||
'/hints/id',
|
||||
'/hints/questionid',
|
||||
'/options/id',
|
||||
'/options/questionid',
|
||||
];
|
||||
$excludefields = array_unique(array_merge($excludefields, $defaultexcludes));
|
||||
|
||||
foreach ($excludefields as $excludefield) {
|
||||
$pathparts = explode('/', ltrim($excludefield, '/'));
|
||||
$data = $questiondata;
|
||||
self::unset_excluded_fields($data, $pathparts);
|
||||
}
|
||||
|
||||
return $questiondata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through the elements of path to an excluded field, and unset the final element.
|
||||
*
|
||||
* If any of the elements in the path is an array, this is called recursively on each element in the array to unset fields
|
||||
* in each child of the array.
|
||||
*
|
||||
* @param stdClass|array $data The questiondata object, or a subsection of it.
|
||||
* @param array $pathparts The remaining elements in the path to the excluded field.
|
||||
* @return void
|
||||
*/
|
||||
private static function unset_excluded_fields(stdClass|array $data, array $pathparts): void {
|
||||
$element = array_shift($pathparts);
|
||||
if (!isset($data->{$element})) {
|
||||
// This element is not present in the data structure, nothing to unset.
|
||||
return;
|
||||
}
|
||||
if (is_object($data->{$element})) {
|
||||
self::unset_excluded_fields($data->{$element}, $pathparts);
|
||||
} else if (is_array($data->{$element})) {
|
||||
foreach ($data->{$element} as $item) {
|
||||
self::unset_excluded_fields($item, $pathparts);
|
||||
}
|
||||
} else if (empty($pathparts)) {
|
||||
// This is the last element of the path and it's a scalar value, unset it.
|
||||
unset($data->{$element});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5271,8 +5271,7 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
||||
$potentialhints = $DB->get_records('question_hints',
|
||||
array('questionid' => $newquestionid), '', 'id, hint');
|
||||
foreach ($potentialhints as $potentialhint) {
|
||||
// Clean in the same way than {@link xml_writer::xml_safe_utf8()}.
|
||||
$cleanhint = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is','', $potentialhint->hint); // Clean CTRL chars.
|
||||
$cleanhint = core_text::trim_ctrl_chars($potentialhint->hint); // Clean CTRL chars.
|
||||
$cleanhint = preg_replace("/\r\n|\r/", "\n", $cleanhint); // Normalize line ending.
|
||||
if ($cleanhint === $data->hint) {
|
||||
$newitemid = $data->id;
|
||||
|
||||
@@ -64,6 +64,8 @@ final class quiz_restore_decode_links_test extends \advanced_testcase {
|
||||
|
||||
$questiondata = \question_bank::load_question_data($question->id);
|
||||
|
||||
$DB->set_field('question', 'questiontext', $CFG->wwwroot . '/mod/quiz/view.php?id=' . $quiz->cmid, ['id' => $question->id]);
|
||||
|
||||
$firstanswer = array_shift($questiondata->options->answers);
|
||||
$DB->set_field('question_answers', 'answer', $CFG->wwwroot . '/course/view.php?id=' . $course->id,
|
||||
['id' => $firstanswer->id]);
|
||||
@@ -87,6 +89,7 @@ final class quiz_restore_decode_links_test extends \advanced_testcase {
|
||||
$questionids = [];
|
||||
foreach ($quizquestions as $quizquestion) {
|
||||
if ($quizquestion->questionid) {
|
||||
$this->assertEquals($CFG->wwwroot . '/mod/quiz/view.php?id=' . $quiz->cmid, $quizquestion->questiontext);
|
||||
$questionids[] = $quizquestion->questionid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -567,7 +567,7 @@ abstract class restore_dbops {
|
||||
* @return array A separate list of all error and warnings detected
|
||||
*/
|
||||
public static function prechek_precheck_qbanks_by_level($restoreid, $courseid, $userid, $samesite, $contextlevel) {
|
||||
global $DB;
|
||||
global $DB, $CFG;
|
||||
|
||||
// To return any errors and warnings found
|
||||
$errors = array();
|
||||
@@ -669,21 +669,37 @@ abstract class restore_dbops {
|
||||
} else {
|
||||
self::set_backup_ids_record($restoreid, 'question_category', $category->id, $matchcat->id, $targetcontext->id);
|
||||
$questions = self::restore_get_questions($restoreid, $category->id);
|
||||
$transformer = self::get_backup_xml_transformer($courseid);
|
||||
|
||||
// Collect all the questions for this category into memory so we only talk to the DB once.
|
||||
$questioncache = $DB->get_records_sql_menu('SELECT q.stamp, q.id
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv
|
||||
ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe
|
||||
ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} qc
|
||||
ON qc.id = qbe.questioncategoryid
|
||||
WHERE qc.id = ?', array($matchcat->id));
|
||||
$recordset = $DB->get_recordset_sql(
|
||||
"SELECT q.*
|
||||
FROM {question} q
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
||||
WHERE qc.id = ?",
|
||||
[$matchcat->id],
|
||||
);
|
||||
|
||||
// Compute a hash of question and answer fields to differentiate between identical stamp-version questions.
|
||||
$questioncache = [];
|
||||
foreach ($recordset as $question) {
|
||||
$question->export_process = true; // Include all question options required for export.
|
||||
get_question_options($question);
|
||||
unset($question->export_process);
|
||||
// Remove some additional properties from get_question_options() that isn't included in backups
|
||||
// before we produce the identity hash.
|
||||
unset($question->categoryobject);
|
||||
unset($question->questioncategoryid);
|
||||
$cachekey = restore_questions_parser_processor::generate_question_identity_hash($question, $transformer);
|
||||
$questioncache[$cachekey] = $question->id;
|
||||
}
|
||||
$recordset->close();
|
||||
|
||||
foreach ($questions as $question) {
|
||||
if (isset($questioncache[$question->stamp])) {
|
||||
$matchqid = $questioncache[$question->stamp];
|
||||
if (isset($questioncache[$question->questionhash])) {
|
||||
$matchqid = $questioncache[$question->questionhash];
|
||||
} else {
|
||||
$matchqid = false;
|
||||
}
|
||||
@@ -1918,6 +1934,22 @@ abstract class restore_dbops {
|
||||
private static function password_should_be_discarded(#[\SensitiveParameter] string $password): bool {
|
||||
return (bool) preg_match('/^[0-9a-f]{32}$/', $password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load required classes and return a backup XML transformer for the specified course.
|
||||
*
|
||||
* These classes may not have been loaded if we're only doing a restore in the current process,
|
||||
* so make sure we have them here.
|
||||
*
|
||||
* @param int $courseid
|
||||
* @return backup_xml_transformer
|
||||
*/
|
||||
protected static function get_backup_xml_transformer(int $courseid): backup_xml_transformer {
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
|
||||
require_once($CFG->dirroot . '/backup/moodle2/backup_plan_builder.class.php');
|
||||
return new backup_xml_transformer($courseid);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -45,6 +45,9 @@ class restore_questions_parser_processor extends grouped_parser_processor {
|
||||
/** @var string XML path in the questions.xml to question elements within question_category (before Moodle 4.0). */
|
||||
protected const LEGACY_QUESTION_SUBPATH = '/questions/question';
|
||||
|
||||
/** @var string String for concatenating data into a string for hashing.*/
|
||||
protected const HASHDATA_SEPARATOR = '|HASHDATA|';
|
||||
|
||||
/** @var string identifies the current restore. */
|
||||
protected string $restoreid;
|
||||
|
||||
@@ -52,13 +55,44 @@ class restore_questions_parser_processor extends grouped_parser_processor {
|
||||
protected int $lastcatid;
|
||||
|
||||
public function __construct($restoreid) {
|
||||
global $CFG;
|
||||
$this->restoreid = $restoreid;
|
||||
$this->lastcatid = 0;
|
||||
parent::__construct();
|
||||
// Set the paths we are interested on
|
||||
$this->add_path(self::CATEGORY_PATH);
|
||||
$this->add_path(self::CATEGORY_PATH . self::QUESTION_SUBPATH);
|
||||
$this->add_path(self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH);
|
||||
$this->add_path(self::CATEGORY_PATH . self::QUESTION_SUBPATH, true);
|
||||
$this->add_path(self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH, true);
|
||||
|
||||
// Add all sub-elements, including those from plugins, as grouped paths with the question tag so that
|
||||
// we can create a hash of all question data for comparison with questions in the database.
|
||||
$this->add_path(self::CATEGORY_PATH . self::QUESTION_SUBPATH . '/question_hints');
|
||||
$this->add_path(self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH . '/question_hints');
|
||||
$this->add_path(self::CATEGORY_PATH . self::QUESTION_SUBPATH . '/question_hints/question_hint');
|
||||
$this->add_path(self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH . '/question_hints/question_hint');
|
||||
|
||||
$connectionpoint = new restore_path_element('question', self::CATEGORY_PATH . self::QUESTION_SUBPATH);
|
||||
foreach (\core\plugin_manager::instance()->get_plugins_of_type('qtype') as $qtype) {
|
||||
$restore = $this->get_qtype_restore($qtype->name);
|
||||
if (!$restore) {
|
||||
continue;
|
||||
}
|
||||
$structure = $restore->define_plugin_structure($connectionpoint);
|
||||
foreach ($structure as $element) {
|
||||
$subpath = str_replace(self::CATEGORY_PATH . self::QUESTION_SUBPATH . '/', '', $element->get_path());
|
||||
$pathparts = explode('/', $subpath);
|
||||
$path = self::CATEGORY_PATH . self::QUESTION_SUBPATH;
|
||||
$legacypath = self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH;
|
||||
foreach ($pathparts as $part) {
|
||||
$path .= '/' . $part;
|
||||
$legacypath .= '/' . $part;
|
||||
if (!in_array($path, $this->paths)) {
|
||||
$this->add_path($path);
|
||||
$this->add_path($legacypath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function dispatch_chunk($data) {
|
||||
@@ -73,10 +107,19 @@ class restore_questions_parser_processor extends grouped_parser_processor {
|
||||
// Prepare question record
|
||||
} else if ($data['path'] == self::CATEGORY_PATH . self::QUESTION_SUBPATH ||
|
||||
$data['path'] == self::CATEGORY_PATH . self::LEGACY_QUESTION_SUBPATH) {
|
||||
$info = (object)$data['tags'];
|
||||
// Remove sub-elements from the question info we're going to save.
|
||||
$info = (object) array_filter($data['tags'], fn($tag) => !is_array($tag));
|
||||
$itemname = 'question';
|
||||
$itemid = $info->id;
|
||||
$parentitemid = $this->lastcatid;
|
||||
$restore = $this->get_qtype_restore($data['tags']['qtype']);
|
||||
if ($restore) {
|
||||
$questiondata = $restore->convert_backup_to_questiondata($data['tags']);
|
||||
} else {
|
||||
$questiondata = restore_qtype_plugin::convert_backup_to_questiondata($data['tags']);
|
||||
}
|
||||
// Store a hash of question fields for comparison with existing questions.
|
||||
$info->questionhash = $this->generate_question_identity_hash($questiondata);
|
||||
|
||||
// Not question_category nor question, impossible. Throw exception.
|
||||
} else {
|
||||
@@ -106,4 +149,80 @@ class restore_questions_parser_processor extends grouped_parser_processor {
|
||||
}
|
||||
return $cdata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and instantiate the restore class for the given question type.
|
||||
*
|
||||
* If there is no restore class, null is returned.
|
||||
*
|
||||
* @param string $qtype The question type name (no qtype_ prefix)
|
||||
* @return ?restore_qtype_plugin
|
||||
*/
|
||||
protected static function get_qtype_restore(string $qtype): ?restore_qtype_plugin {
|
||||
global $CFG;
|
||||
$step = new restore_quiz_activity_structure_step('questions', 'question.xml');
|
||||
$filepath = "{$CFG->dirroot}/question/type/{$qtype}/backup/moodle2/restore_qtype_{$qtype}_plugin.class.php";
|
||||
if (!file_exists($filepath)) {
|
||||
return null;
|
||||
}
|
||||
require_once($filepath);
|
||||
$restoreclass = "restore_qtype_{$qtype}_plugin";
|
||||
if (!class_exists($restoreclass)) {
|
||||
return null;
|
||||
}
|
||||
return new $restoreclass('qtype', $qtype, $step);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a data structure containing the data for a question, reduce it to a flat array and return a sha1 hash of the data.
|
||||
*
|
||||
* @param stdClass $questiondata An array containing all the data for a question, including hints and qtype plugin data.
|
||||
* @param ?backup_xml_transformer $transformer If provided, run the backup transformer process on all text fields. This ensures
|
||||
* that values from the database are compared like-for-like with encoded values from the backup.
|
||||
* @return string A sha1 hash of all question data, normalised and concatenated together.
|
||||
*/
|
||||
public static function generate_question_identity_hash(
|
||||
stdClass $questiondata,
|
||||
?backup_xml_transformer $transformer = null,
|
||||
): string {
|
||||
$questiondata = clone($questiondata);
|
||||
$restore = self::get_qtype_restore($questiondata->qtype);
|
||||
if ($restore) {
|
||||
$restore->define_plugin_structure(new restore_path_element('question', self::CATEGORY_PATH . self::QUESTION_SUBPATH));
|
||||
// Combine default exclusions with those specified by the plugin.
|
||||
$questiondata = $restore->remove_excluded_question_data($questiondata, $restore->get_excluded_identity_hash_fields());
|
||||
} else {
|
||||
// The qtype has no restore class, use the default reduction method.
|
||||
$questiondata = restore_qtype_plugin::remove_excluded_question_data($questiondata);
|
||||
}
|
||||
|
||||
// Convert questiondata to a flat array of values.
|
||||
$hashdata = [];
|
||||
// Convert the object to a multi-dimensional array for compatibility with array_walk_recursive.
|
||||
$questiondata = json_decode(json_encode($questiondata), true);
|
||||
array_walk_recursive($questiondata, function($value) use (&$hashdata) {
|
||||
// Normalise data types. Depending on where the data comes from, it may be a mixture of nulls, strings,
|
||||
// ints and floats. Convert everything to strings, then all numbers to floats to ensure we are doing
|
||||
// like-for-like comparisons without losing accuracy.
|
||||
$value = (string) $value;
|
||||
if (is_numeric($value)) {
|
||||
$value = (float) ($value);
|
||||
} else if (str_contains($value, "\r\n")) {
|
||||
// Normalise line breaks.
|
||||
$value = str_replace("\r\n", "\n", $value);
|
||||
}
|
||||
$hashdata[] = $value;
|
||||
});
|
||||
|
||||
sort($hashdata, SORT_STRING);
|
||||
$hashstring = implode(self::HASHDATA_SEPARATOR, $hashdata);
|
||||
if ($transformer) {
|
||||
$hashstring = $transformer->process($hashstring);
|
||||
// Need to re-sort the hashdata with the transformed strings.
|
||||
$hashdata = explode(self::HASHDATA_SEPARATOR, $hashstring);
|
||||
sort($hashdata, SORT_STRING);
|
||||
$hashstring = implode(self::HASHDATA_SEPARATOR, $hashdata);
|
||||
}
|
||||
return sha1($hashstring);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,14 +253,14 @@ class xml_writer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform some UTF-8 cleaning, stripping the control chars (\x0-\x1f)
|
||||
* but tabs (\x9), newlines (\xa) and returns (\xd). The delete control
|
||||
* Perform some UTF-8 cleaning, stripping the control chars (\x00-\x1f)
|
||||
* but tabs (\x09), newlines (\xa) and returns (\xd). The delete control
|
||||
* char (\x7f) is also included. All them are forbiden in XML 1.0 specs.
|
||||
* The expression below seems to be UTF-8 safe too because it simply
|
||||
* ignores the rest of characters. Also normalize linefeeds and return chars.
|
||||
*/
|
||||
protected function xml_safe_utf8($content) {
|
||||
$content = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is', '', $content ?? ''); // clean CTRL chars.
|
||||
$content = core_text::trim_ctrl_chars($content ?? '');
|
||||
$content = preg_replace("/\r\n|\r/", "\n", $content); // Normalize line&return=>line
|
||||
return fix_utf8($content);
|
||||
}
|
||||
|
||||
@@ -82,6 +82,16 @@ if (!$badge->is_active()) {
|
||||
die();
|
||||
}
|
||||
|
||||
if (!empty($role)) {
|
||||
if (!user_has_role_assignment($USER->id, $role, $context->id) && !$isadmin) {
|
||||
// User does not have the role passed by the parameter.
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->notification(get_string('wrongrole', 'badges'));
|
||||
echo $OUTPUT->footer();
|
||||
die();
|
||||
}
|
||||
}
|
||||
|
||||
$returnurl = new moodle_url('recipients.php', array('id' => $badge->id));
|
||||
$returnlink = html_writer::link($returnurl, $strrecipients);
|
||||
$actionbar = new \core_badges\output\standard_action_bar($PAGE, $badge->type, false, false, $returnurl);
|
||||
|
||||
+112
-58
@@ -60,6 +60,7 @@ if (!empty($issuedbadge->recipient->id)) {
|
||||
$assertionid = $assertion->get_assertion_hash();
|
||||
$assertionentityid = $assertiondata['id'];
|
||||
$badgeadded = false;
|
||||
$issuerexists = false;
|
||||
if (badges_open_badges_backpack_api() == OPEN_BADGES_V2) {
|
||||
$sitebackpack = badges_get_site_primary_backpack();
|
||||
$api = new \core_badges\backpack_api($sitebackpack);
|
||||
@@ -71,62 +72,75 @@ if (!empty($issuedbadge->recipient->id)) {
|
||||
$issuer = $assertion->get_issuer();
|
||||
if (!($issuerentityid = badges_external_get_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_ISSUER, $issuer['email']))) {
|
||||
$response = $api->put_issuer($issuer);
|
||||
if (!$response) {
|
||||
throw new moodle_exception('invalidrequest', 'error');
|
||||
if ($response) {
|
||||
$issuerexists = true;
|
||||
$issuerentityid = $response->id;
|
||||
badges_external_create_mapping(
|
||||
$sitebackpack->id,
|
||||
OPEN_BADGES_V2_TYPE_ISSUER,
|
||||
$issuer['email'],
|
||||
$issuerentityid,
|
||||
);
|
||||
}
|
||||
$issuerentityid = $response->id;
|
||||
badges_external_create_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_ISSUER, $issuer['email'],
|
||||
$issuerentityid);
|
||||
}
|
||||
// Create badge.
|
||||
$badge = $assertion->get_badge_class(false);
|
||||
$badgeid = $assertion->get_badge_id();
|
||||
if (!($badgeentityid = badges_external_get_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_BADGE, $badgeid))) {
|
||||
$response = $api->put_badgeclass($issuerentityid, $badge);
|
||||
if (!$response) {
|
||||
throw new moodle_exception('invalidrequest', 'error');
|
||||
if ($issuerexists) {
|
||||
// Create badge.
|
||||
$badge = $assertion->get_badge_class(false);
|
||||
$badgeid = $assertion->get_badge_id();
|
||||
if (!($badgeentityid = badges_external_get_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_BADGE, $badgeid))) {
|
||||
$response = $api->put_badgeclass($issuerentityid, $badge);
|
||||
if ($response) {
|
||||
$badgeentityid = $response->id;
|
||||
badges_external_create_mapping(
|
||||
$sitebackpack->id,
|
||||
OPEN_BADGES_V2_TYPE_BADGE,
|
||||
$badgeid,
|
||||
$badgeentityid,
|
||||
);
|
||||
}
|
||||
}
|
||||
$badgeentityid = $response->id;
|
||||
badges_external_create_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_BADGE, $badgeid,
|
||||
$badgeentityid);
|
||||
}
|
||||
|
||||
// Create assertion (Award the badge!).
|
||||
$assertionentityid = badges_external_get_mapping(
|
||||
$sitebackpack->id,
|
||||
OPEN_BADGES_V2_TYPE_ASSERTION,
|
||||
$assertionid
|
||||
);
|
||||
|
||||
if ($assertionentityid && strpos($sitebackpack->backpackapiurl, 'badgr')) {
|
||||
$assertionentityid = badges_generate_badgr_open_url(
|
||||
$sitebackpack,
|
||||
OPEN_BADGES_V2_TYPE_ASSERTION,
|
||||
$assertionentityid
|
||||
);
|
||||
}
|
||||
|
||||
// Create an assertion for the recipient in the issuer's account.
|
||||
if (!$assertionentityid) {
|
||||
$response = $api->put_badgeclass_assertion($badgeentityid, $assertiondata);
|
||||
if (!$response) {
|
||||
throw new moodle_exception('invalidrequest', 'error');
|
||||
}
|
||||
$assertionentityid = badges_generate_badgr_open_url($sitebackpack, OPEN_BADGES_V2_TYPE_ASSERTION, $response->id);
|
||||
$badgeadded = true;
|
||||
badges_external_create_mapping($sitebackpack->id, OPEN_BADGES_V2_TYPE_ASSERTION, $assertionid,
|
||||
$response->id);
|
||||
} else {
|
||||
// An assertion already exists. Make sure it's up to date.
|
||||
$internalid = badges_external_get_mapping(
|
||||
// Create assertion (Award the badge!).
|
||||
$assertionentityid = badges_external_get_mapping(
|
||||
$sitebackpack->id,
|
||||
OPEN_BADGES_V2_TYPE_ASSERTION,
|
||||
$assertionid,
|
||||
'externalid'
|
||||
$assertionid
|
||||
);
|
||||
$response = $api->update_assertion($internalid, $assertiondata);
|
||||
if (!$response) {
|
||||
throw new moodle_exception('invalidrequest', 'error');
|
||||
|
||||
if ($assertionentityid && strpos($sitebackpack->backpackapiurl, 'badgr')) {
|
||||
$assertionentityid = badges_generate_badgr_open_url(
|
||||
$sitebackpack,
|
||||
OPEN_BADGES_V2_TYPE_ASSERTION,
|
||||
$assertionentityid
|
||||
);
|
||||
}
|
||||
|
||||
// Create an assertion for the recipient in the issuer's account.
|
||||
if (!$assertionentityid) {
|
||||
$response = $api->put_badgeclass_assertion($badgeentityid, $assertiondata);
|
||||
if ($response) {
|
||||
$assertionentityid = badges_generate_badgr_open_url(
|
||||
$sitebackpack,
|
||||
OPEN_BADGES_V2_TYPE_ASSERTION,
|
||||
$response->id,
|
||||
);
|
||||
$badgeadded = true;
|
||||
badges_external_create_mapping(
|
||||
$sitebackpack->id,
|
||||
OPEN_BADGES_V2_TYPE_ASSERTION,
|
||||
$assertionid,
|
||||
$response->id,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// An assertion already exists. Make sure it's up to date.
|
||||
$internalid = badges_external_get_mapping(
|
||||
$sitebackpack->id,
|
||||
OPEN_BADGES_V2_TYPE_ASSERTION,
|
||||
$assertionid,
|
||||
'externalid'
|
||||
);
|
||||
$response = $api->update_assertion($internalid, $assertiondata);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,21 +150,61 @@ if (!empty($issuedbadge->recipient->id)) {
|
||||
// - If a user and site backpack have the same provider we can skip this as Badgr automatically maps recipients
|
||||
// based on email address.
|
||||
// - This is only needed when the backpacks are from different regions.
|
||||
if ($assertionentityid && !badges_external_get_mapping($userbackpack->id, OPEN_BADGES_V2_TYPE_ASSERTION, $assertionid)) {
|
||||
if (
|
||||
$assertionentityid
|
||||
&& (!$issuerexists || !badges_external_get_mapping($userbackpack->id, OPEN_BADGES_V2_TYPE_ASSERTION, $assertionid))
|
||||
) {
|
||||
$userapi = new \core_badges\backpack_api($userbackpack, $backpack);
|
||||
$userapi->authenticate();
|
||||
$response = $userapi->import_badge_assertion($assertionentityid);
|
||||
if (!$response) {
|
||||
throw new moodle_exception('invalidrequest', 'error');
|
||||
if ($response) {
|
||||
$assertionentityid = $response->id;
|
||||
$badgeadded = true;
|
||||
badges_external_create_mapping(
|
||||
$userbackpack->id,
|
||||
OPEN_BADGES_V2_TYPE_ASSERTION,
|
||||
$assertionid,
|
||||
$assertionentityid,
|
||||
);
|
||||
}
|
||||
$assertionentityid = $response->id;
|
||||
$badgeadded = true;
|
||||
badges_external_create_mapping($userbackpack->id, OPEN_BADGES_V2_TYPE_ASSERTION, $assertionid,
|
||||
$assertionentityid);
|
||||
}
|
||||
|
||||
$response = $badgeadded ? ['success' => 'addedtobackpack'] : ['warning' => 'existsinbackpack'];
|
||||
redirect(new moodle_url('/badges/mybadges.php', $response));
|
||||
if ($badgeadded) {
|
||||
$message = get_string('addedtobackpack', 'badges');
|
||||
$messagetype = \core\output\notification::NOTIFY_SUCCESS;
|
||||
} else {
|
||||
if (isset($userapi) && !empty($userapi->get_errors())) {
|
||||
// If the api used to import the badge to the backpack has errors, show them to inform the user.
|
||||
if (array_filter($userapi->get_errors(), fn($element) => str_contains($element, "DUPLICATE_BADGE"))) {
|
||||
// Duplicated badges are displayed as a warning.
|
||||
$message = get_string('existsinbackpack', 'badges');
|
||||
$messagetype = \core\output\notification::NOTIFY_WARNING;
|
||||
} else {
|
||||
// If the userapi has any other errors, we will use those to inform the user.
|
||||
$message = get_string(
|
||||
'error:cannotsendtobackpack',
|
||||
'badges',
|
||||
implode($userapi->get_errors()),
|
||||
);
|
||||
$messagetype = \core\output\notification::NOTIFY_ERROR;
|
||||
}
|
||||
} else if (isset($api) && !empty($api->get_errors())) {
|
||||
// If the api used to create/update the issuer has errors, show them to inform the user.
|
||||
$errors = $api->get_errors() ?? [get_string('invalidrequest', 'error')];
|
||||
$message = get_string(
|
||||
'error:cannotsendtobackpack',
|
||||
'badges',
|
||||
implode($errors),
|
||||
);
|
||||
$messagetype = \core\output\notification::NOTIFY_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
redirect(
|
||||
url: new moodle_url('/badges/mybadges.php'),
|
||||
message: $message,
|
||||
messagetype: $messagetype,
|
||||
);
|
||||
} else {
|
||||
redirect(new moodle_url('/badges/mybadges.php'));
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ if ($action == 'delete' && $confirm && confirm_sesskey()) {
|
||||
} else {
|
||||
$msg = get_string('sitebackpacknotdeleted', 'badges');
|
||||
}
|
||||
} else if ($action == 'moveup' || $action == 'movedown') {
|
||||
} else if (($action == 'moveup' || $action == 'movedown') && confirm_sesskey()) {
|
||||
// If no backpack has been selected, there isn't anything to move.
|
||||
if (empty($id)) {
|
||||
redirect($url);
|
||||
|
||||
@@ -668,6 +668,20 @@ class backpack_api {
|
||||
return backpack_api_mapping::get_authentication_error();
|
||||
}
|
||||
|
||||
/**
|
||||
* List all errors occurred during the requests to the backpack.
|
||||
*
|
||||
* @return array The list of errors.
|
||||
*/
|
||||
public function get_errors(): array {
|
||||
$errors = [];
|
||||
foreach ($this->mappings as $mapping) {
|
||||
$errors = array_merge($errors, $mapping->get_errors());
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of badges in a collection.
|
||||
*
|
||||
|
||||
@@ -85,6 +85,9 @@ class backpack_api_mapping {
|
||||
/** @var int OpenBadges version 1 or 2. */
|
||||
protected $backpackapiversion;
|
||||
|
||||
/** @var array Errors encountered during the request. */
|
||||
protected $errors = [];
|
||||
|
||||
/**
|
||||
* Create a mapping.
|
||||
*
|
||||
@@ -150,6 +153,26 @@ class backpack_api_mapping {
|
||||
return self::$authenticationerror;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the errors encountered during the request.
|
||||
*
|
||||
* @return array The list of errors.
|
||||
*/
|
||||
public function get_errors() {
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an error to the list of errors.
|
||||
*
|
||||
* @param string $error The error message.
|
||||
* @return self This instance for method chaining.
|
||||
*/
|
||||
public function add_error(string $error): self {
|
||||
$this->errors[] = $error;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the action match this mapping?
|
||||
*
|
||||
@@ -372,6 +395,23 @@ class backpack_api_mapping {
|
||||
$response = $curl->put($url, $post, $options);
|
||||
}
|
||||
$response = json_decode($response);
|
||||
if ($response === null) {
|
||||
$this->add_error(get_string('invalidrequest', 'error'));
|
||||
return null;
|
||||
}
|
||||
if (isset($response->status) && isset($response->status->success) && $response->status->success != true) {
|
||||
// If the response wasn't successful, store the errors and return null.
|
||||
if (isset($response->validationErrors)) {
|
||||
$error = implode(', ', $response->validationErrors);
|
||||
} else if (isset($response->status->description)) {
|
||||
$error = $response->status->description;
|
||||
} else {
|
||||
$error = get_string('invalidrequest', 'error');
|
||||
}
|
||||
$this->add_error($error);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isset($response->result)) {
|
||||
$response = $response->result;
|
||||
}
|
||||
|
||||
@@ -56,13 +56,13 @@
|
||||
<td> {{{backpackweburl}}} </td>
|
||||
<td>
|
||||
{{#canmoveup}}
|
||||
<a href="{{baseurl}}?id={{id}}&action=moveup">{{#pix}}t/up, core,{{#str}}moveup{{/str}}{{/pix}}</a>
|
||||
<a href="{{baseurl}}?id={{id}}&sesskey={{sesskey}}&action=moveup">{{#pix}}t/up, core,{{#str}}moveup{{/str}}{{/pix}}</a>
|
||||
{{/canmoveup}}
|
||||
{{^canmoveup}}
|
||||
{{#pix}}spacer, moodle{{/pix}}
|
||||
{{/canmoveup}}
|
||||
{{#canmovedown}}
|
||||
<a href="{{baseurl}}?id={{id}}&action=movedown">{{#pix}}t/down, core,{{#str}}movedown{{/str}}{{/pix}}</a>
|
||||
<a href="{{baseurl}}?id={{id}}&sesskey={{sesskey}}&action=movedown">{{#pix}}t/down, core,{{#str}}movedown{{/str}}{{/pix}}</a>
|
||||
{{/canmovedown}}
|
||||
{{^canmovedown}}
|
||||
{{#pix}}spacer, moodle{{/pix}}
|
||||
|
||||
@@ -14,13 +14,13 @@ Feature: Award badges based on competency completion
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| user1 | C1 | student |
|
||||
And the following lp "frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| Framework 1 | sc-y-2 |
|
||||
And the following lp "competencies" exist:
|
||||
| shortname | framework |
|
||||
| comp1 | sc-y-2 |
|
||||
| comp2 | sc-y-2 |
|
||||
And the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| Framework 1 | sc-y-2 |
|
||||
And the following "core_competency > competencies" exist:
|
||||
| shortname | competencyframework |
|
||||
| comp1 | sc-y-2 |
|
||||
| comp2 | sc-y-2 |
|
||||
And I log in as "admin"
|
||||
|
||||
@javascript
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core_badges\tests;
|
||||
|
||||
/**
|
||||
* Helper trait for external function tests.
|
||||
*
|
||||
* @package core_badges
|
||||
* @copyright 2025 Moodle Pty Ltd
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
trait external_helper {
|
||||
|
||||
/**
|
||||
* Asserts that an issued badge returned by an external function matches the given data.
|
||||
*
|
||||
* @param array $expected Expected badge data.
|
||||
* @param array $actual Actual badge data returned by the external function.
|
||||
* @param bool $isrecipient True if user is the badge recipient.
|
||||
* @param bool $canconfiguredetails True if user has capability "moodle/badges:configuredetails".
|
||||
* @throws \PHPUnit\Framework\ExpectationFailedException
|
||||
*/
|
||||
protected function assert_issued_badge(array $expected, array $actual, bool $isrecipient, bool $canconfiguredetails): void {
|
||||
$this->assertEquals($expected['id'], $actual['id']);
|
||||
$this->assertEquals($expected['name'], $actual['name']);
|
||||
$this->assertEquals($expected['type'], $actual['type']);
|
||||
$this->assertEquals($expected['description'], $actual['description']);
|
||||
$this->assertEquals($expected['issuername'], $actual['issuername']);
|
||||
$this->assertEquals($expected['issuerurl'], $actual['issuerurl']);
|
||||
$this->assertEquals($expected['issuercontact'], $actual['issuercontact']);
|
||||
$this->assertEquals($expected['uniquehash'], $actual['uniquehash']);
|
||||
$this->assertEquals($expected['dateissued'], $actual['dateissued']);
|
||||
$this->assertEquals($expected['dateexpire'], $actual['dateexpire']);
|
||||
$this->assertEquals($expected['version'], $actual['version']);
|
||||
$this->assertEquals($expected['language'], $actual['language']);
|
||||
$this->assertEquals($expected['imageauthorname'], $actual['imageauthorname']);
|
||||
$this->assertEquals($expected['imageauthoremail'], $actual['imageauthoremail']);
|
||||
$this->assertEquals($expected['imageauthorurl'], $actual['imageauthorurl']);
|
||||
$this->assertEquals($expected['imagecaption'], $actual['imagecaption']);
|
||||
$this->assertEquals($expected['badgeurl'], $actual['badgeurl']);
|
||||
$this->assertEquals($expected['endorsement'] ?? null, $actual['endorsement'] ?? null);
|
||||
|
||||
if ($isrecipient || $canconfiguredetails) {
|
||||
$this->assertTimeCurrent($expected['timecreated']);
|
||||
$this->assertTimeCurrent($expected['timemodified']);
|
||||
$this->assertEquals($expected['usercreated'], $actual['usercreated']);
|
||||
$this->assertEquals($expected['usermodified'], $actual['usermodified']);
|
||||
$this->assertEquals($expected['expiredate'], $actual['expiredate']);
|
||||
$this->assertEquals($expected['expireperiod'], $actual['expireperiod']);
|
||||
$this->assertEquals($expected['courseid'], $actual['courseid']);
|
||||
$this->assertEquals($expected['message'], $actual['message']);
|
||||
$this->assertEquals($expected['messagesubject'], $actual['messagesubject']);
|
||||
$this->assertEquals($expected['attachment'], $actual['attachment']);
|
||||
$this->assertEquals($expected['notification'], $actual['notification']);
|
||||
$this->assertEquals($expected['nextcron'], $actual['nextcron']);
|
||||
$this->assertEquals($expected['status'], $actual['status']);
|
||||
$this->assertEquals($expected['issuedid'], $actual['issuedid']);
|
||||
$this->assertEquals($expected['visible'], $actual['visible']);
|
||||
$this->assertEquals($expected['email'], $actual['email']);
|
||||
} else {
|
||||
$this->assertEquals(0, $actual['timecreated']);
|
||||
$this->assertEquals(0, $actual['timemodified']);
|
||||
$this->assertArrayNotHasKey('usercreated', $actual);
|
||||
$this->assertArrayNotHasKey('usermodified', $actual);
|
||||
$this->assertArrayNotHasKey('expiredate', $actual);
|
||||
$this->assertArrayNotHasKey('expireperiod', $actual);
|
||||
$this->assertArrayNotHasKey('courseid', $actual);
|
||||
$this->assertArrayNotHasKey('message', $actual);
|
||||
$this->assertArrayNotHasKey('messagesubject', $actual);
|
||||
$this->assertEquals(1, $actual['attachment']);
|
||||
$this->assertEquals(1, $actual['notification']);
|
||||
$this->assertArrayNotHasKey('nextcron', $actual);
|
||||
$this->assertEquals(0, $actual['status']);
|
||||
$this->assertArrayNotHasKey('issuedid', $actual);
|
||||
$this->assertEquals(0, $actual['visible']);
|
||||
$this->assertArrayNotHasKey('email', $actual);
|
||||
}
|
||||
|
||||
$alignments = $expected['alignment'];
|
||||
if (!$canconfiguredetails) {
|
||||
foreach ($alignments as $index => $alignment) {
|
||||
$alignments[$index] = [
|
||||
'id' => $alignment['id'],
|
||||
'badgeid' => $alignment['badgeid'],
|
||||
'targetName' => $alignment['targetName'],
|
||||
'targetUrl' => $alignment['targetUrl'],
|
||||
];
|
||||
}
|
||||
}
|
||||
$this->assertEquals($alignments, $actual['alignment']);
|
||||
|
||||
$relatedbadges = $expected['relatedbadges'];
|
||||
if (!$canconfiguredetails) {
|
||||
foreach ($relatedbadges as $index => $relatedbadge) {
|
||||
$relatedbadges[$index] = [
|
||||
'id' => $relatedbadge['id'],
|
||||
'name' => $relatedbadge['name'],
|
||||
];
|
||||
}
|
||||
}
|
||||
$this->assertEquals($relatedbadges, $actual['relatedbadges']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns test data for external functions.
|
||||
*
|
||||
* The test data includes:
|
||||
* - A teacher.
|
||||
* - A student.
|
||||
* - A site badge, with an endorsment and 2 alignments, issued to the student by the teacher.
|
||||
* - A course badge, related to the site badge, issued to the student by the teacher.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function prepare_test_data(): array {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
$generator = $this->getDataGenerator();
|
||||
|
||||
// Setup test data.
|
||||
$course = $generator->create_course();
|
||||
|
||||
// Create users and enrolments.
|
||||
$student = $generator->create_and_enrol($course);
|
||||
$teacher = $generator->create_and_enrol($course, 'editingteacher');
|
||||
$badgegenerator = $generator->get_plugin_generator('core_badges');
|
||||
|
||||
$systemcontext = \context_system::instance();
|
||||
$coursecontext = \context_course::instance($course->id);
|
||||
|
||||
// Create a site badge.
|
||||
$now = time();
|
||||
$sitebadge = $badgegenerator->create_badge([
|
||||
'name' => "Test badge site",
|
||||
'description' => "Testing badges site",
|
||||
'timecreated' => $now,
|
||||
'timemodified' => $now,
|
||||
'usercreated' => (int) $teacher->id,
|
||||
'usermodified' => (int) $teacher->id,
|
||||
'expiredate' => $now + YEARSECS,
|
||||
'expireperiod' => YEARSECS,
|
||||
'type' => BADGE_TYPE_SITE,
|
||||
]);
|
||||
|
||||
$sitebadge->issue($student->id, true);
|
||||
$siteissuedbadge = $DB->get_record('badge_issued', [ 'badgeid' => $sitebadge->id ]);
|
||||
|
||||
// Change issued time to ensure badges are fetched in a consistent order.
|
||||
$siteissuedbadge->dateissued = $now - 1;
|
||||
$DB->update_record('badge_issued', $siteissuedbadge);
|
||||
|
||||
$sitebadgedata = [
|
||||
...(array) $sitebadge,
|
||||
'issuedid' => (int) $siteissuedbadge->id,
|
||||
'uniquehash' => $siteissuedbadge->uniquehash,
|
||||
'dateissued' => (int) $siteissuedbadge->dateissued,
|
||||
'dateexpire' => $siteissuedbadge->dateexpire,
|
||||
'visible' => (int) $siteissuedbadge->visible,
|
||||
'badgeurl' => \moodle_url::make_webservice_pluginfile_url($systemcontext->id, 'badges', 'badgeimage',
|
||||
$sitebadge->id, '/', 'f3')->out(false),
|
||||
'email' => $student->email,
|
||||
'endorsement' => null,
|
||||
'alignment' => [],
|
||||
'relatedbadges' => [],
|
||||
];
|
||||
|
||||
// Add an endorsement for the site badge.
|
||||
$endorsement = new \stdClass();
|
||||
$endorsement->badgeid = $sitebadge->id;
|
||||
$endorsement->issuername = 'Issuer name';
|
||||
$endorsement->issuerurl = 'http://endorsement-issuer-url.domain.co.nz';
|
||||
$endorsement->issueremail = 'endorsementissuer@example.com';
|
||||
$endorsement->claimid = 'http://claim-url.domain.co.nz';
|
||||
$endorsement->claimcomment = 'Claim comment';
|
||||
$endorsement->dateissued = $now;
|
||||
$endorsement->id = $sitebadge->save_endorsement($endorsement);
|
||||
$sitebadgedata['endorsement'] = (array) $endorsement;
|
||||
|
||||
// Add 2 alignments to the site badge.
|
||||
$alignment = new \stdClass();
|
||||
$alignment->badgeid = $sitebadge->id;
|
||||
$alignment->targetname = 'Alignment 1';
|
||||
$alignment->targeturl = 'http://a1-target-url.domain.co.nz';
|
||||
$alignment->targetdescription = 'A1 target description';
|
||||
$alignment->targetframework = 'A1 framework';
|
||||
$alignment->targetcode = 'A1 code';
|
||||
$alignment->id = $sitebadge->save_alignment($alignment);
|
||||
$sitebadgedata['alignment'][] = [
|
||||
'id' => $alignment->id,
|
||||
'badgeid' => $alignment->badgeid,
|
||||
'targetName' => $alignment->targetname,
|
||||
'targetUrl' => $alignment->targeturl,
|
||||
'targetDescription' => $alignment->targetdescription,
|
||||
'targetFramework' => $alignment->targetframework,
|
||||
'targetCode' => $alignment->targetcode,
|
||||
];
|
||||
|
||||
$alignment = new \stdClass();
|
||||
$alignment->badgeid = $sitebadge->id;
|
||||
$alignment->targetname = 'Alignment 2';
|
||||
$alignment->targeturl = 'http://a2-target-url.domain.co.nz';
|
||||
$alignment->targetdescription = 'A2 target description';
|
||||
$alignment->targetframework = 'A2 framework';
|
||||
$alignment->targetcode = 'A2 code';
|
||||
$alignment->id = $sitebadge->save_alignment($alignment);
|
||||
$sitebadgedata['alignment'][] = [
|
||||
'id' => $alignment->id,
|
||||
'badgeid' => $alignment->badgeid,
|
||||
'targetName' => $alignment->targetname,
|
||||
'targetUrl' => $alignment->targeturl,
|
||||
'targetDescription' => $alignment->targetdescription,
|
||||
'targetFramework' => $alignment->targetframework,
|
||||
'targetCode' => $alignment->targetcode,
|
||||
];
|
||||
|
||||
// Create a course badge.
|
||||
$coursebadge = $badgegenerator->create_badge([
|
||||
'name' => "Test badge course",
|
||||
'description' => "Testing badges course",
|
||||
'timecreated' => $now,
|
||||
'timemodified' => $now,
|
||||
'usercreated' => (int) $teacher->id,
|
||||
'usermodified' => (int) $teacher->id,
|
||||
'expiredate' => $now + YEARSECS,
|
||||
'expireperiod' => YEARSECS,
|
||||
'type' => BADGE_TYPE_COURSE,
|
||||
'courseid' => (int) $course->id,
|
||||
]);
|
||||
$coursebadge->issue($student->id, true);
|
||||
|
||||
$courseissuedbadge = $DB->get_record('badge_issued', [ 'badgeid' => $coursebadge->id ]);
|
||||
$coursebadgedata = [
|
||||
...(array) $coursebadge,
|
||||
'issuedid' => (int) $courseissuedbadge->id,
|
||||
'uniquehash' => $courseissuedbadge->uniquehash,
|
||||
'dateissued' => (int) $courseissuedbadge->dateissued,
|
||||
'dateexpire' => $courseissuedbadge->dateexpire,
|
||||
'visible' => (int) $courseissuedbadge->visible,
|
||||
'badgeurl' => \moodle_url::make_webservice_pluginfile_url($coursecontext->id, 'badges', 'badgeimage',
|
||||
$coursebadge->id, '/', 'f3')->out(false),
|
||||
'email' => $student->email,
|
||||
'endorsement' => null,
|
||||
'alignment' => [],
|
||||
'relatedbadges' => [],
|
||||
];
|
||||
|
||||
// Add the course badge to the site badge.
|
||||
$sitebadge->add_related_badges([$coursebadge->id]);
|
||||
$sitebadgedata['relatedbadges'][] = [
|
||||
'id' => (int) $coursebadge->id,
|
||||
'name' => $coursebadge->name,
|
||||
'version' => $coursebadge->version,
|
||||
'language' => $coursebadge->language,
|
||||
'type' => $coursebadge->type,
|
||||
];
|
||||
$coursebadgedata['relatedbadges'][] = [
|
||||
'id' => (int) $sitebadge->id,
|
||||
'name' => $sitebadge->name,
|
||||
'version' => $sitebadge->version,
|
||||
'language' => $sitebadge->language,
|
||||
'type' => $sitebadge->type,
|
||||
];
|
||||
|
||||
return [
|
||||
'coursebadge' => $coursebadgedata,
|
||||
'sitebadge' => $sitebadgedata,
|
||||
'student' => $student,
|
||||
'teacher' => $teacher,
|
||||
];
|
||||
}
|
||||
}
|
||||
+31
-185
@@ -27,6 +27,7 @@
|
||||
namespace core_badges\external;
|
||||
|
||||
use core_badges_external;
|
||||
use core_badges\tests\external_helper;
|
||||
use core_external\external_api;
|
||||
use core_external\external_settings;
|
||||
use externallib_advanced_testcase;
|
||||
@@ -48,221 +49,66 @@ require_once($CFG->libdir . '/badgeslib.php');
|
||||
* @since Moodle 3.1
|
||||
*/
|
||||
final class external_test extends externallib_advanced_testcase {
|
||||
|
||||
/** @var stdClass $course */
|
||||
private $course;
|
||||
|
||||
/** @var stdClass $student */
|
||||
private $student;
|
||||
|
||||
/** @var stdClass $teacher */
|
||||
private $teacher;
|
||||
|
||||
/**
|
||||
* Set up for every test
|
||||
*/
|
||||
public function setUp(): void {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Setup test data.
|
||||
$this->course = $this->getDataGenerator()->create_course();
|
||||
|
||||
// Create users and enrolments.
|
||||
$this->student = $this->getDataGenerator()->create_and_enrol($this->course, 'student');
|
||||
$this->teacher = $this->getDataGenerator()->create_and_enrol($this->course, 'editingteacher');
|
||||
|
||||
// Mock up a site badge.
|
||||
$now = time();
|
||||
$badge = new \stdClass();
|
||||
$badge->id = null;
|
||||
$badge->name = "Test badge site";
|
||||
$badge->description = "Testing badges site";
|
||||
$badge->timecreated = $now - 12;
|
||||
$badge->timemodified = $now - 12;
|
||||
$badge->usercreated = $this->teacher->id;
|
||||
$badge->usermodified = $this->teacher->id;
|
||||
$badge->issuername = "Test issuer";
|
||||
$badge->issuerurl = "http://issuer-url.domain.co.nz";
|
||||
$badge->issuercontact = "issuer@example.com";
|
||||
$badge->expiredate = null;
|
||||
$badge->expireperiod = null;
|
||||
$badge->type = BADGE_TYPE_SITE;
|
||||
$badge->courseid = null;
|
||||
$badge->messagesubject = "Test message subject for badge";
|
||||
$badge->message = "Test message body for badge";
|
||||
$badge->attachment = 1;
|
||||
$badge->notification = 0;
|
||||
$badge->status = BADGE_STATUS_ACTIVE;
|
||||
$badge->version = '1';
|
||||
$badge->language = 'en';
|
||||
$badge->imageauthorname = 'Image author';
|
||||
$badge->imageauthoremail = 'imageauthor@example.com';
|
||||
$badge->imageauthorurl = 'http://image-author-url.domain.co.nz';
|
||||
$badge->imagecaption = 'Caption';
|
||||
|
||||
$badgeid = $DB->insert_record('badge', $badge, true);
|
||||
$badge = new \badge($badgeid);
|
||||
$badge->issue($this->student->id, true);
|
||||
|
||||
// Hack the database to adjust the time each badge was issued.
|
||||
$DB->set_field('badge_issued', 'dateissued', $now - 11, array('userid' => $this->student->id, 'badgeid' => $badgeid));
|
||||
|
||||
// Add an endorsement for the badge.
|
||||
$endorsement = new \stdClass();
|
||||
$endorsement->badgeid = $badgeid;
|
||||
$endorsement->issuername = 'Issuer name';
|
||||
$endorsement->issuerurl = 'http://endorsement-issuer-url.domain.co.nz';
|
||||
$endorsement->issueremail = 'endorsementissuer@example.com';
|
||||
$endorsement->claimid = 'http://claim-url.domain.co.nz';
|
||||
$endorsement->claimcomment = 'Claim comment';
|
||||
$endorsement->dateissued = $now;
|
||||
$badge->save_endorsement($endorsement);
|
||||
|
||||
// Add 2 alignments.
|
||||
$alignment = new \stdClass();
|
||||
$alignment->badgeid = $badgeid;
|
||||
$alignment->targetname = 'Alignment 1';
|
||||
$alignment->targeturl = 'http://a1-target-url.domain.co.nz';
|
||||
$alignment->targetdescription = 'A1 target description';
|
||||
$alignment->targetframework = 'A1 framework';
|
||||
$alignment->targetcode = 'A1 code';
|
||||
$badge->save_alignment($alignment);
|
||||
|
||||
$alignment->targetname = 'Alignment 2';
|
||||
$alignment->targeturl = 'http://a2-target-url.domain.co.nz';
|
||||
$alignment->targetdescription = 'A2 target description';
|
||||
$alignment->targetframework = 'A2 framework';
|
||||
$alignment->targetcode = 'A2 code';
|
||||
$badge->save_alignment($alignment);
|
||||
|
||||
// Now a course badge.
|
||||
$badge->id = null;
|
||||
$badge->name = "Test badge course";
|
||||
$badge->description = "Testing badges course";
|
||||
$badge->type = BADGE_TYPE_COURSE;
|
||||
$badge->courseid = $this->course->id;
|
||||
|
||||
$coursebadgeid = $DB->insert_record('badge', $badge, true);
|
||||
$badge = new \badge($coursebadgeid);
|
||||
$badge->issue($this->student->id, true);
|
||||
|
||||
// Hack the database to adjust the time each badge was issued.
|
||||
$DB->set_field('badge_issued', 'dateissued', $now - 10, array('userid' => $this->student->id, 'badgeid' => $coursebadgeid));
|
||||
|
||||
// Make the site badge a related badge.
|
||||
$badge->add_related_badges(array($badgeid));
|
||||
}
|
||||
use external_helper;
|
||||
|
||||
/**
|
||||
* Test get user badges.
|
||||
* These is a basic test since the badges_get_my_user_badges used by the external function already has unit tests.
|
||||
*
|
||||
* @covers \core_badges_external::get_user_badges
|
||||
*/
|
||||
public function test_get_my_user_badges(): void {
|
||||
$data = $this->prepare_test_data();
|
||||
|
||||
$this->setUser($this->student);
|
||||
|
||||
$badges = (array) badges_get_user_badges($this->student->id);
|
||||
$expectedbadges = array();
|
||||
$coursebadge = null;
|
||||
|
||||
foreach ($badges as $badge) {
|
||||
$context = ($badge->type == BADGE_TYPE_SITE) ? \context_system::instance() : \context_course::instance($badge->courseid);
|
||||
$badge->badgeurl = \moodle_url::make_webservice_pluginfile_url($context->id, 'badges', 'badgeimage', $badge->id, '/',
|
||||
'f3')->out(false);
|
||||
|
||||
// Get the endorsement, alignments and related badges.
|
||||
$badgeinstance = new \badge($badge->id);
|
||||
$endorsement = $badgeinstance->get_endorsement();
|
||||
$alignments = $badgeinstance->get_alignments();
|
||||
$relatedbadges = $badgeinstance->get_related_badges();
|
||||
$badge->alignment = array();
|
||||
$badge->relatedbadges = array();
|
||||
|
||||
if ($endorsement) {
|
||||
$badge->endorsement = (array) $endorsement;
|
||||
}
|
||||
|
||||
if (!empty($alignments)) {
|
||||
foreach ($alignments as $alignment) {
|
||||
// Students cannot see some fields of the alignments.
|
||||
unset($alignment->targetname);
|
||||
unset($alignment->targeturl);
|
||||
unset($alignment->targetdescription);
|
||||
unset($alignment->targetframework);
|
||||
unset($alignment->targetcode);
|
||||
|
||||
$badge->alignment[] = (array) $alignment;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($relatedbadges)) {
|
||||
foreach ($relatedbadges as $relatedbadge) {
|
||||
// Students cannot see some fields of the related badges.
|
||||
unset($relatedbadge->version);
|
||||
unset($relatedbadge->language);
|
||||
unset($relatedbadge->type);
|
||||
|
||||
$badge->relatedbadges[] = (array) $relatedbadge;
|
||||
}
|
||||
}
|
||||
|
||||
$expectedbadges[] = (array) $badge;
|
||||
if (isset($badge->courseid)) {
|
||||
// Save the course badge to be able to compare it in our tests.
|
||||
$coursebadge = (array) $badge;
|
||||
}
|
||||
}
|
||||
|
||||
$this->setUser($data['student']);
|
||||
$result = core_badges_external::get_user_badges();
|
||||
$result = external_api::clean_returnvalue(core_badges_external::get_user_badges_returns(), $result);
|
||||
$this->assertEquals($expectedbadges, $result['badges']);
|
||||
$this->assertCount(2, $result['badges']);
|
||||
$this->assert_issued_badge($data['coursebadge'], $result['badges'][0], true, false);
|
||||
$this->assert_issued_badge($data['sitebadge'], $result['badges'][1], true, false);
|
||||
|
||||
// Pagination and filtering.
|
||||
$result = core_badges_external::get_user_badges(0, $this->course->id, 0, 1, '', true);
|
||||
$result = core_badges_external::get_user_badges(0, 0, 0, 1, '', true);
|
||||
$result = external_api::clean_returnvalue(core_badges_external::get_user_badges_returns(), $result);
|
||||
$this->assertCount(1, $result['badges']);
|
||||
$this->assertEquals($coursebadge, $result['badges'][0]);
|
||||
$this->assert_issued_badge($data['coursebadge'], $result['badges'][0], true, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get user badges.
|
||||
*
|
||||
* @covers \core_badges_external::get_user_badges
|
||||
*/
|
||||
public function test_get_other_user_badges(): void {
|
||||
$data = $this->prepare_test_data();
|
||||
|
||||
$this->setUser($this->teacher);
|
||||
|
||||
$result = core_badges_external::get_user_badges($this->student->id);
|
||||
// User with "moodle/badges:configuredetails" capability.
|
||||
$this->setAdminUser();
|
||||
$result = core_badges_external::get_user_badges($data['student']->id);
|
||||
$result = external_api::clean_returnvalue(core_badges_external::get_user_badges_returns(), $result);
|
||||
|
||||
$this->assertCount(2, $result['badges']);
|
||||
$this->assert_issued_badge($data['coursebadge'], $result['badges'][0], false, true);
|
||||
$this->assert_issued_badge($data['sitebadge'], $result['badges'][1], false, true);
|
||||
|
||||
// Check that we don't have permissions for view the complete information for site badges.
|
||||
foreach ($result['badges'] as $badge) {
|
||||
if (isset($badge['type']) and $badge['type'] == BADGE_TYPE_COURSE) {
|
||||
$this->assertTrue(isset($badge['message']));
|
||||
|
||||
// Check that we have permissions to see all the data in alignments and related badges.
|
||||
foreach ($badge['alignment'] as $alignment) {
|
||||
$this->assertTrue(isset($alignment['targetdescription']));
|
||||
}
|
||||
|
||||
foreach ($badge['relatedbadges'] as $relatedbadge) {
|
||||
$this->assertTrue(isset($relatedbadge['type']));
|
||||
}
|
||||
} else {
|
||||
$this->assertFalse(isset($badge['message']));
|
||||
}
|
||||
}
|
||||
// User without "moodle/badges:configuredetails" capability.
|
||||
$this->setUser($this->getDataGenerator()->create_user());
|
||||
$result = core_badges_external::get_user_badges($data['student']->id);
|
||||
$result = external_api::clean_returnvalue(core_badges_external::get_user_badges_returns(), $result);
|
||||
$this->assertCount(2, $result['badges']);
|
||||
$this->assert_issued_badge($data['coursebadge'], $result['badges'][0], false, false);
|
||||
$this->assert_issued_badge($data['sitebadge'], $result['badges'][1], false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get_user_badges where issuername contains text to be filtered
|
||||
*
|
||||
* @covers \core_badges_external::get_user_badges
|
||||
*/
|
||||
public function test_get_user_badges_filter_issuername(): void {
|
||||
global $DB;
|
||||
|
||||
$data = $this->prepare_test_data();
|
||||
|
||||
filter_set_global_state('multilang', TEXTFILTER_ON);
|
||||
filter_set_applies_to_strings('multilang', true);
|
||||
|
||||
@@ -273,7 +119,7 @@ final class external_test extends externallib_advanced_testcase {
|
||||
$DB->set_field('badge', 'issuername', $issuername, ['name' => 'Test badge site']);
|
||||
|
||||
// Retrieve student badges.
|
||||
$result = core_badges_external::get_user_badges($this->student->id);
|
||||
$result = core_badges_external::get_user_badges($data['student']->id);
|
||||
$result = external_api::clean_returnvalue(core_badges_external::get_user_badges_returns(), $result);
|
||||
|
||||
// Site badge will be last, because it has the earlier issued date.
|
||||
|
||||
+38
-187
@@ -16,6 +16,7 @@
|
||||
|
||||
namespace core_badges\external;
|
||||
|
||||
use core_badges\tests\external_helper;
|
||||
use externallib_advanced_testcase;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
@@ -23,7 +24,6 @@ defined('MOODLE_INTERNAL') || die();
|
||||
global $CFG;
|
||||
|
||||
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
|
||||
require_once($CFG->libdir . '/badgeslib.php');
|
||||
|
||||
/**
|
||||
* Tests for external function get_user_badge_by_hash.
|
||||
@@ -36,141 +36,7 @@ require_once($CFG->libdir . '/badgeslib.php');
|
||||
* @coversDefaultClass \core_badges\external\get_user_badge_by_hash
|
||||
*/
|
||||
final class get_user_badge_by_hash_test extends externallib_advanced_testcase {
|
||||
|
||||
/**
|
||||
* Prepare the test.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function prepare_test_data(): array {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Setup test data.
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
|
||||
// Create users and enrolments.
|
||||
$student1 = $this->getDataGenerator()->create_and_enrol($course);
|
||||
$student2 = $this->getDataGenerator()->create_and_enrol($course);
|
||||
$teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
|
||||
|
||||
// Mock up a site badge.
|
||||
$now = time();
|
||||
$badge = new \stdClass();
|
||||
$badge->id = null;
|
||||
$badge->name = "Test badge site";
|
||||
$badge->description = "Testing badges site";
|
||||
$badge->timecreated = $now;
|
||||
$badge->timemodified = $now;
|
||||
$badge->usercreated = (int) $teacher->id;
|
||||
$badge->usermodified = (int) $teacher->id;
|
||||
$badge->expiredate = null;
|
||||
$badge->expireperiod = null;
|
||||
$badge->type = BADGE_TYPE_SITE;
|
||||
$badge->courseid = null;
|
||||
$badge->messagesubject = "Test message subject for badge";
|
||||
$badge->message = "Test message body for badge";
|
||||
$badge->attachment = 1;
|
||||
$badge->notification = 0;
|
||||
$badge->status = BADGE_STATUS_ACTIVE;
|
||||
$badge->version = '1';
|
||||
$badge->language = 'en';
|
||||
$badge->imageauthorname = 'Image author';
|
||||
$badge->imageauthoremail = 'imageauthor@example.com';
|
||||
$badge->imageauthorurl = 'http://image-author-url.domain.co.nz';
|
||||
$badge->imagecaption = 'Caption';
|
||||
|
||||
$badgeid = $DB->insert_record('badge', $badge, true);
|
||||
$badge->id = $badgeid;
|
||||
$sitebadge = new \badge($badgeid);
|
||||
$sitebadge->issue($student1->id, true);
|
||||
$siteissuedbadge = $DB->get_record('badge_issued', [ 'badgeid' => $badge->id ]);
|
||||
|
||||
$badge->issuername = $sitebadge->issuername;
|
||||
$badge->issuercontact = $sitebadge->issuercontact;
|
||||
$badge->issuerurl = $sitebadge->issuerurl;
|
||||
$badge->nextcron = $sitebadge->nextcron;
|
||||
$badge->issuedid = (int) $siteissuedbadge->id;
|
||||
$badge->uniquehash = $siteissuedbadge->uniquehash;
|
||||
$badge->dateissued = (int) $siteissuedbadge->dateissued;
|
||||
$badge->dateexpire = $siteissuedbadge->dateexpire;
|
||||
$badge->visible = (int) $siteissuedbadge->visible;
|
||||
$badge->email = $student1->email;
|
||||
$context = \context_system::instance();
|
||||
$badge->badgeurl = \moodle_url::make_webservice_pluginfile_url($context->id, 'badges', 'badgeimage', $badge->id, '/',
|
||||
'f3')->out(false);
|
||||
$badge->status = BADGE_STATUS_ACTIVE_LOCKED;
|
||||
|
||||
// Add an endorsement for the badge.
|
||||
$endorsement = new \stdClass();
|
||||
$endorsement->badgeid = $badgeid;
|
||||
$endorsement->issuername = 'Issuer name';
|
||||
$endorsement->issuerurl = 'http://endorsement-issuer-url.domain.co.nz';
|
||||
$endorsement->issueremail = 'endorsementissuer@example.com';
|
||||
$endorsement->claimid = 'http://claim-url.domain.co.nz';
|
||||
$endorsement->claimcomment = 'Claim comment';
|
||||
$endorsement->dateissued = $now;
|
||||
$endorsement->id = $sitebadge->save_endorsement($endorsement);
|
||||
$badge->endorsement = (array) $endorsement;
|
||||
|
||||
// Add 2 alignments.
|
||||
$alignment = new \stdClass();
|
||||
$alignment->badgeid = $badgeid;
|
||||
$alignment->id = $sitebadge->save_alignment($alignment);
|
||||
$badge->alignment[] = (array) $alignment;
|
||||
|
||||
$alignment->id = $sitebadge->save_alignment($alignment);
|
||||
$badge->alignment[] = (array) $alignment;
|
||||
$badge->relatedbadges = [];
|
||||
$usersitebadge[] = (array) $badge;
|
||||
|
||||
// Now a course badge.
|
||||
$badge->id = null;
|
||||
$badge->name = "Test badge course";
|
||||
$badge->description = "Testing badges course";
|
||||
$badge->type = BADGE_TYPE_COURSE;
|
||||
$badge->courseid = (int) $course->id;
|
||||
|
||||
$badge->id = $DB->insert_record('badge', $badge, true);
|
||||
$coursebadge = new \badge($badge->id );
|
||||
$coursebadge->issue($student1->id, true);
|
||||
$courseissuedbadge = $DB->get_record('badge_issued', [ 'badgeid' => $badge->id ]);
|
||||
|
||||
$badge->issuername = $coursebadge->issuername;
|
||||
$badge->issuercontact = $coursebadge->issuercontact;
|
||||
$badge->issuerurl = $coursebadge->issuerurl;
|
||||
$badge->nextcron = $coursebadge->nextcron;
|
||||
$badge->issuedid = (int) $courseissuedbadge->id;
|
||||
$badge->uniquehash = $courseissuedbadge->uniquehash;
|
||||
$badge->dateissued = (int) $courseissuedbadge->dateissued;
|
||||
$badge->dateexpire = $courseissuedbadge->dateexpire;
|
||||
$badge->visible = (int) $courseissuedbadge->visible;
|
||||
$badge->email = $student1->email;
|
||||
$context = \context_course::instance($badge->courseid);
|
||||
$badge->badgeurl = \moodle_url::make_webservice_pluginfile_url($context->id, 'badges', 'badgeimage', $badge->id , '/',
|
||||
'f3')->out(false);
|
||||
|
||||
unset($badge->endorsement);
|
||||
$badge->alignment = [];
|
||||
$usercoursebadge[] = (array) $badge;
|
||||
// Make the site badge a related badge.
|
||||
$sitebadge->add_related_badges([$badge->id]);
|
||||
$usersitebadge[0]['relatedbadges'][0] = [
|
||||
'id' => (int) $coursebadge->id,
|
||||
'name' => $coursebadge->name
|
||||
];
|
||||
$usercoursebadge[0]['relatedbadges'][0] = [
|
||||
'id' => (int) $sitebadge->id,
|
||||
'name' => $sitebadge->name
|
||||
];
|
||||
return [
|
||||
'coursebadge' => $usercoursebadge,
|
||||
'sitebadge' => $usersitebadge,
|
||||
'student1' => $student1,
|
||||
'student2' => $student2
|
||||
];
|
||||
}
|
||||
use external_helper;
|
||||
|
||||
/**
|
||||
* Test get user badge by hash.
|
||||
@@ -179,18 +45,47 @@ final class get_user_badge_by_hash_test extends externallib_advanced_testcase {
|
||||
*/
|
||||
public function test_get_user_badge_by_hash(): void {
|
||||
$data = $this->prepare_test_data();
|
||||
$this->setUser($data['student1']);
|
||||
|
||||
// Site badge.
|
||||
$result = get_user_badge_by_hash::execute($data['sitebadge'][0]['uniquehash']);
|
||||
// Site badge fetched by recipient.
|
||||
$this->setUser($data['student']);
|
||||
$result = get_user_badge_by_hash::execute($data['sitebadge']['uniquehash']);
|
||||
$result = \core_external\external_api::clean_returnvalue(get_user_badge_by_hash::execute_returns(), $result);
|
||||
$this->assertEquals($data['sitebadge'][0]['uniquehash'], $result['badge'][0]['uniquehash']);
|
||||
$this->assert_issued_badge($data['sitebadge'], $result['badge'][0], true, false);
|
||||
$this->assertEmpty($result['warnings']);
|
||||
|
||||
// Course badge.
|
||||
$result = get_user_badge_by_hash::execute($data['coursebadge'][0]['uniquehash']);
|
||||
// Site badge fetched by user without "moodle/badges:configuredetails" capability.
|
||||
$this->setGuestUser();
|
||||
$result = get_user_badge_by_hash::execute($data['sitebadge']['uniquehash']);
|
||||
$result = \core_external\external_api::clean_returnvalue(get_user_badge_by_hash::execute_returns(), $result);
|
||||
$this->assertEquals($data['coursebadge'][0]['uniquehash'], $result['badge'][0]['uniquehash']);
|
||||
$this->assert_issued_badge($data['sitebadge'], $result['badge'][0], false, false);
|
||||
$this->assertEmpty($result['warnings']);
|
||||
|
||||
// Site badge fetched by user with "moodle/badges:configuredetails" capability.
|
||||
$this->setAdminUser();
|
||||
$result = get_user_badge_by_hash::execute($data['sitebadge']['uniquehash']);
|
||||
$result = \core_external\external_api::clean_returnvalue(get_user_badge_by_hash::execute_returns(), $result);
|
||||
$this->assert_issued_badge($data['sitebadge'], $result['badge'][0], false, true);
|
||||
$this->assertEmpty($result['warnings']);
|
||||
|
||||
// Course badge fetched by recipient.
|
||||
$this->setUser($data['student']);
|
||||
$result = get_user_badge_by_hash::execute($data['coursebadge']['uniquehash']);
|
||||
$result = \core_external\external_api::clean_returnvalue(get_user_badge_by_hash::execute_returns(), $result);
|
||||
$this->assert_issued_badge($data['coursebadge'], $result['badge'][0], true, false);
|
||||
$this->assertEmpty($result['warnings']);
|
||||
|
||||
// Course badge fetched by user without "moodle/badges:configuredetails" capability.
|
||||
$this->setGuestUser();
|
||||
$result = get_user_badge_by_hash::execute($data['coursebadge']['uniquehash']);
|
||||
$result = \core_external\external_api::clean_returnvalue(get_user_badge_by_hash::execute_returns(), $result);
|
||||
$this->assert_issued_badge($data['coursebadge'], $result['badge'][0], false, false);
|
||||
$this->assertEmpty($result['warnings']);
|
||||
|
||||
// Course badge fetched by user with "moodle/badges:configuredetails" capability.
|
||||
$this->setAdminUser();
|
||||
$result = get_user_badge_by_hash::execute($data['coursebadge']['uniquehash']);
|
||||
$result = \core_external\external_api::clean_returnvalue(get_user_badge_by_hash::execute_returns(), $result);
|
||||
$this->assert_issued_badge($data['coursebadge'], $result['badge'][0], false, true);
|
||||
$this->assertEmpty($result['warnings']);
|
||||
|
||||
// Wrong hash.
|
||||
@@ -200,48 +95,4 @@ final class get_user_badge_by_hash_test extends externallib_advanced_testcase {
|
||||
$this->assertNotEmpty($result['warnings']);
|
||||
$this->assertEquals('badgeawardnotfound', $result['warnings'][0]['warningcode']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test get user badge by hash with restrictions.
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_get_user_badge_by_hash_with_restrictions(): void {
|
||||
$data = $this->prepare_test_data();
|
||||
$this->setUser($data['student2']);
|
||||
|
||||
// Site badge.
|
||||
$result = get_user_badge_by_hash::execute($data['sitebadge'][0]['uniquehash']);
|
||||
$result = \core_external\external_api::clean_returnvalue(get_user_badge_by_hash::execute_returns(), $result);
|
||||
$this->assertNotEmpty($result['badge']);
|
||||
$this->assertEmpty($result['warnings']);
|
||||
|
||||
// Check that we don't have permissions for view the complete information for site badges.
|
||||
if (isset($result['badge'][0]['type']) && $result['badge'][0]['type'] == BADGE_TYPE_SITE) {
|
||||
$this->assertFalse(isset($result['badge'][0]['message']));
|
||||
|
||||
// Check that we have permissions to see all the data in alignments and related badges.
|
||||
foreach ($result['badge'][0]['alignment'] as $alignment) {
|
||||
$this->assertTrue(isset($alignment['id']));
|
||||
}
|
||||
|
||||
foreach ($result['badge'][0]['relatedbadges'] as $relatedbadge) {
|
||||
$this->assertTrue(isset($relatedbadge['id']));
|
||||
}
|
||||
} else {
|
||||
$this->assertTrue(isset($result['badge'][0]['message']));
|
||||
}
|
||||
|
||||
// Course badge.
|
||||
$result = get_user_badge_by_hash::execute($data['coursebadge'][0]['uniquehash']);
|
||||
$result = \core_external\external_api::clean_returnvalue(get_user_badge_by_hash::execute_returns(), $result);
|
||||
$this->assertNotEmpty($result['badge']);
|
||||
$this->assertEmpty($result['warnings']);
|
||||
|
||||
// Check that we don't have permissions for view the complete information for course badges.
|
||||
if (isset($result['badge'][0]['type']) && $result['badge'][0]['type'] == BADGE_TYPE_COURSE) {
|
||||
$this->assertFalse(isset($result['badge'][0]['message']));
|
||||
} else {
|
||||
$this->assertTrue(isset($result['badge'][0]['message']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Script to let a user edit the properties of a particular RSS feed.
|
||||
* Script to let a user view the output of a particular RSS feed.
|
||||
*
|
||||
* @package block_rss_client
|
||||
* @copyright 2009 Tim Hunt
|
||||
@@ -26,9 +26,6 @@ require_once(__DIR__ . '/../../config.php');
|
||||
require_once($CFG->libdir .'/simplepie/moodle_simplepie.php');
|
||||
|
||||
require_login();
|
||||
if (isguestuser()) {
|
||||
throw new \moodle_exception('guestsarenotallowed');
|
||||
}
|
||||
|
||||
$returnurl = optional_param('returnurl', '', PARAM_LOCALURL);
|
||||
$courseid = optional_param('courseid', 0, PARAM_INT);
|
||||
@@ -46,6 +43,11 @@ if ($courseid) {
|
||||
$PAGE->set_context($context);
|
||||
}
|
||||
|
||||
$managesharedfeeds = has_capability('block/rss_client:manageanyfeeds', $context);
|
||||
if (!$managesharedfeeds) {
|
||||
require_capability('block/rss_client:manageownfeeds', $context);
|
||||
}
|
||||
|
||||
$urlparams = array('rssid' => $rssid);
|
||||
if ($courseid) {
|
||||
$urlparams['courseid'] = $courseid;
|
||||
@@ -56,10 +58,18 @@ if ($returnurl) {
|
||||
$PAGE->set_url('/blocks/rss_client/viewfeed.php', $urlparams);
|
||||
$PAGE->set_pagelayout('popup');
|
||||
|
||||
$rssrecord = $DB->get_record('block_rss_client', array('id' => $rssid), '*', MUST_EXIST);
|
||||
if ($managesharedfeeds) {
|
||||
$select = 'id = :id AND (userid = :userid OR shared = 1)';
|
||||
} else {
|
||||
$select = 'id = :id AND userid = :userid';
|
||||
}
|
||||
|
||||
$rssrecord = $DB->get_record_select('block_rss_client', $select, [
|
||||
'id' => $rssid,
|
||||
'userid' => $USER->id,
|
||||
], '*', MUST_EXIST);
|
||||
|
||||
$rss = new moodle_simplepie($rssrecord->url);
|
||||
|
||||
if ($rss->error()) {
|
||||
debugging($rss->error());
|
||||
throw new \moodle_exception('errorfetchingrssfeed');
|
||||
|
||||
+26
-17
@@ -34,15 +34,21 @@ $PAGE->set_url(new moodle_url('/blog/external_blogs.php'));
|
||||
require_capability('moodle/blog:manageexternal', $context);
|
||||
|
||||
$delete = optional_param('delete', null, PARAM_INT);
|
||||
$confirm = optional_param('confirm', false, PARAM_BOOL);
|
||||
|
||||
$strexternalblogs = get_string('externalblogs', 'blog');
|
||||
$straddnewexternalblog = get_string('addnewexternalblog', 'blog');
|
||||
$strblogs = get_string('blogs', 'blog');
|
||||
$message = null;
|
||||
|
||||
if ($delete && confirm_sesskey()) {
|
||||
$externalblog = $DB->get_record('blog_external', array('id' => $delete));
|
||||
if ($externalblog->userid == $USER->id) {
|
||||
$PAGE->set_title("{$strblogs}: {$strexternalblogs}");
|
||||
$PAGE->set_pagelayout('standard');
|
||||
|
||||
if ($delete) {
|
||||
$externalblog = $DB->get_record('blog_external', ['id' => $delete, 'userid' => $USER->id], '*', MUST_EXIST);
|
||||
|
||||
if ($confirm) {
|
||||
require_sesskey();
|
||||
|
||||
// Delete the external blog.
|
||||
$DB->delete_records('blog_external', array('id' => $delete));
|
||||
|
||||
@@ -60,23 +66,28 @@ if ($delete && confirm_sesskey()) {
|
||||
$event = \core\event\blog_external_removed::create($eventparms);
|
||||
$event->add_record_snapshot('blog_external', $externalblog);
|
||||
$event->trigger();
|
||||
$message = get_string('externalblogdeleted', 'blog');
|
||||
|
||||
redirect($PAGE->url, get_string('externalblogdeleted', 'blog'));
|
||||
} else {
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->heading("{$strexternalblogs}: " . s($externalblog->name), 2);
|
||||
|
||||
echo $OUTPUT->confirm(
|
||||
get_string('deleteexternalblog', 'blog'),
|
||||
new moodle_url($PAGE->url->out_omit_querystring(), ['delete' => $delete, 'confirm' => 1]),
|
||||
$PAGE->url,
|
||||
);
|
||||
|
||||
echo $OUTPUT->footer();
|
||||
die;
|
||||
}
|
||||
}
|
||||
|
||||
$blogs = $DB->get_records('blog_external', array('userid' => $USER->id));
|
||||
|
||||
$PAGE->set_heading(fullname($USER));
|
||||
$PAGE->set_title("$strblogs: $strexternalblogs");
|
||||
$PAGE->set_pagelayout('standard');
|
||||
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->heading($strexternalblogs, 2);
|
||||
|
||||
if (!empty($message)) {
|
||||
echo $OUTPUT->notification($message);
|
||||
}
|
||||
|
||||
echo $OUTPUT->box_start('generalbox boxaligncenter');
|
||||
|
||||
if (!empty($blogs)) {
|
||||
@@ -99,10 +110,8 @@ if (!empty($blogs)) {
|
||||
$editurl = new moodle_url('/blog/external_blog_edit.php', array('id' => $blog->id));
|
||||
$editicon = $OUTPUT->action_icon($editurl, new pix_icon('t/edit', get_string('editexternalblog', 'blog')));
|
||||
|
||||
$deletelink = new moodle_url('/blog/external_blogs.php', array('delete' => $blog->id, 'sesskey' => sesskey()));
|
||||
$action = new confirm_action(get_string('externalblogdeleteconfirm', 'blog'));
|
||||
$deleteicon = $OUTPUT->action_icon($deletelink, new pix_icon('t/delete', get_string('deleteexternalblog', 'blog')),
|
||||
$action);
|
||||
$deletelink = new moodle_url('/blog/external_blogs.php', ['delete' => $blog->id]);
|
||||
$deleteicon = $OUTPUT->action_icon($deletelink, new pix_icon('t/delete', get_string('deleteexternalblog', 'blog')));
|
||||
|
||||
$table->data[] = new html_table_row(array($blog->name,
|
||||
$blog->url,
|
||||
|
||||
@@ -915,10 +915,10 @@ class core_calendar_external extends external_api {
|
||||
$formoptions['eventtypes'] = $allowedeeventtypes;
|
||||
if ($courseid) {
|
||||
require_once($CFG->libdir . '/grouplib.php');
|
||||
$groupcoursedata = groups_get_course_data($courseid);
|
||||
if (!empty($groupcoursedata->groups)) {
|
||||
$groupcoursedata = groups_get_all_groups($courseid);
|
||||
if (!empty($groupcoursedata)) {
|
||||
$formoptions['groups'] = [];
|
||||
foreach ($groupcoursedata->groups as $groupid => $groupdata) {
|
||||
foreach ($groupcoursedata as $groupid => $groupdata) {
|
||||
$formoptions['groups'][$groupid] = $groupdata->name;
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -3639,9 +3639,9 @@ function calendar_output_fragment_event_form($args) {
|
||||
|
||||
if (is_null($eventid)) {
|
||||
if (!empty($courseid)) {
|
||||
$groupcoursedata = groups_get_course_data($courseid);
|
||||
$groupcoursedata = groups_get_all_groups($courseid);
|
||||
$formoptions['groups'] = [];
|
||||
foreach ($groupcoursedata->groups as $groupid => $groupdata) {
|
||||
foreach ($groupcoursedata as $groupid => $groupdata) {
|
||||
$formoptions['groups'][$groupid] = $groupdata->name;
|
||||
}
|
||||
}
|
||||
@@ -3683,9 +3683,9 @@ function calendar_output_fragment_event_form($args) {
|
||||
$formoptions['event'] = $event;
|
||||
|
||||
if (!empty($event->courseid)) {
|
||||
$groupcoursedata = groups_get_course_data($event->courseid);
|
||||
$groupcoursedata = groups_get_all_groups($event->courseid);
|
||||
$formoptions['groups'] = [];
|
||||
foreach ($groupcoursedata->groups as $groupid => $groupdata) {
|
||||
foreach ($groupcoursedata as $groupid => $groupdata) {
|
||||
$formoptions['groups'][$groupid] = $groupdata->name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,8 +41,8 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Set our timezone based on the timezone in the RFC's samples (US/Eastern).
|
||||
$tz = 'US/Eastern';
|
||||
// Set our timezone based on the timezone in the RFC's samples (America/New_York).
|
||||
$tz = 'America/New_York';
|
||||
$this->setTimezone($tz);
|
||||
$timezone = new \DateTimeZone($tz);
|
||||
// Create our event's DTSTART date based on RFC's samples (most commonly used in RFC is 1997-09-02 09:00:00 EDT).
|
||||
@@ -685,8 +685,8 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
|
||||
// Change the start date for forever events to 9am of the 2nd day of the current month and year.
|
||||
$this->change_event_startdate(date('Ym02\T090000'));
|
||||
$startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart));
|
||||
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart));
|
||||
$startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart), new \DateTimeZone('Australia/Perth'));
|
||||
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart), new \DateTimeZone('Australia/Perth'));
|
||||
|
||||
$offsetinterval = $startdatetime->diff($startdate, true);
|
||||
$interval = new \DateInterval('P12M');
|
||||
@@ -842,8 +842,8 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
// Change the start date for forever events to 9am of the 2nd day of the current month and year.
|
||||
$this->change_event_startdate(date('Ym02\T090000'));
|
||||
|
||||
$startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart));
|
||||
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart));
|
||||
$startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart), new \DateTimeZone('Australia/Perth'));
|
||||
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart), new \DateTimeZone('Australia/Perth'));
|
||||
|
||||
$offsetinterval = $startdatetime->diff($startdate, true);
|
||||
$interval = new \DateInterval('P12M');
|
||||
@@ -1201,8 +1201,8 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_other_day_forever(): void {
|
||||
global $DB;
|
||||
|
||||
// Change the start date for forever events to 9am of the current date in US/Eastern time.
|
||||
$this->change_event_startdate(date('Ymd\T090000'), 'US/Eastern');
|
||||
// Change the start date for forever events to 9am of the current date in America/New_York time.
|
||||
$this->change_event_startdate(date('Ymd\T090000'), 'America/New_York');
|
||||
|
||||
$startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart));
|
||||
$interval = new \DateInterval('P2D');
|
||||
@@ -1271,7 +1271,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to 01-01-1998, based on the example from the RFC.
|
||||
$this->change_event_startdate('19980101T090000', 'US/Eastern');
|
||||
$this->change_event_startdate('19980101T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=YEARLY;UNTIL=20000131T090000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -1305,7 +1305,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to 01-01-1998, based on the example from the RFC.
|
||||
$this->change_event_startdate('19980101T090000', 'US/Eastern');
|
||||
$this->change_event_startdate('19980101T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=DAILY;UNTIL=20000131T090000Z;BYMONTH=1';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -1403,8 +1403,8 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_other_week_forever(): void {
|
||||
global $DB;
|
||||
|
||||
// Change the start date for forever events to 9am of the current date in US/Eastern time.
|
||||
$this->change_event_startdate(date('Ymd\T090000'), 'US/Eastern');
|
||||
// Change the start date for forever events to 9am of the current date in America/New_York time.
|
||||
$this->change_event_startdate(date('Ymd\T090000'), 'America/New_York');
|
||||
|
||||
$interval = new \DateInterval('P2W');
|
||||
|
||||
@@ -1614,7 +1614,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to 05-09-1997, based on the example from the RFC.
|
||||
$startdatetime = $this->change_event_startdate('19970905T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970905T090000', 'America/New_York');
|
||||
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart));
|
||||
$offsetinterval = $startdatetime->diff($startdate, true);
|
||||
|
||||
@@ -1650,7 +1650,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to 05-09-1997, based on the example from the RFC.
|
||||
$startdatetime = $this->change_event_startdate('19970905T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970905T090000', 'America/New_York');
|
||||
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart));
|
||||
$offsetinterval = $startdatetime->diff($startdate, true);
|
||||
|
||||
@@ -1688,7 +1688,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to 05-09-1997, based on the example from the RFC.
|
||||
$startdatetime = $this->change_event_startdate('19970907T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970907T090000', 'America/New_York');
|
||||
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart));
|
||||
$offsetinterval = $startdatetime->diff($startdate, true);
|
||||
|
||||
@@ -1731,7 +1731,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to 05-09-1997, based on the example from the RFC.
|
||||
$startdatetime = $this->change_event_startdate('19970922T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970922T090000', 'America/New_York');
|
||||
$startdate = new \DateTime($startdatetime->format('Y-m-d'));
|
||||
$offsetinterval = $startdatetime->diff($startdate, true);
|
||||
|
||||
@@ -1773,7 +1773,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to 28 September of the current year, based on the example from the RFC.
|
||||
$this->change_event_startdate(date('Y0928\T090000'), 'US/Eastern');
|
||||
$this->change_event_startdate(date('Y0928\T090000'), 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=MONTHLY;BYMONTHDAY=-3';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -1859,7 +1859,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_first_and_last_day_of_the_month_10_count(): void {
|
||||
global $DB;
|
||||
|
||||
$startdatetime = $this->change_event_startdate('19970930T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970930T090000', 'America/New_York');
|
||||
$startdate = new \DateTime($startdatetime->format('Y-m-d'));
|
||||
$offsetinterval = $startdatetime->diff($startdate, true);
|
||||
|
||||
@@ -1904,7 +1904,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_18_months_days_10_to_15_10_count(): void {
|
||||
global $DB;
|
||||
|
||||
$startdatetime = $this->change_event_startdate('19970910T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970910T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -1949,9 +1949,9 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_tuesday_every_other_month_forever(): void {
|
||||
global $DB;
|
||||
|
||||
// Change the start date for forever events to 9am of the Tuesday on or before of the current date in US/Eastern time.
|
||||
// Change the start date for forever events to 9am of the Tuesday on or before of the current date in America/New_York time.
|
||||
$nexttuesday = new \DateTime('next Tuesday');
|
||||
$this->change_event_startdate($nexttuesday->format('Ymd\T090000'), 'US/Eastern');
|
||||
$this->change_event_startdate($nexttuesday->format('Ymd\T090000'), 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=MONTHLY;INTERVAL=2;BYDAY=TU';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2004,7 +2004,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_yearly_in_june_july_10_count(): void {
|
||||
global $DB;
|
||||
|
||||
$startdatetime = $this->change_event_startdate('19970610T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970610T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=YEARLY;COUNT=10;BYMONTH=6,7';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2047,7 +2047,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_other_year_in_june_july_10_count(): void {
|
||||
global $DB;
|
||||
|
||||
$startdatetime = $this->change_event_startdate('19970310T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970310T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2094,7 +2094,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_3_years_1st_100th_200th_days_10_count(): void {
|
||||
global $DB;
|
||||
|
||||
$startdatetime = $this->change_event_startdate('19970101T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970101T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2144,7 +2144,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
// Change our event's date to the 20th Monday of the current year.
|
||||
$twentiethmonday = new \DateTime(date('Y-01-01'));
|
||||
$twentiethmonday->modify('+20 Monday');
|
||||
$startdatetime = $this->change_event_startdate($twentiethmonday->format('Ymd\T000000'), 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate($twentiethmonday->format('Ymd\T000000'), 'America/New_York');
|
||||
|
||||
$interval = new \DateInterval('P1Y');
|
||||
|
||||
@@ -2190,7 +2190,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
// Change our event's date to the start of the 20th week of the current year.
|
||||
$twentiethweek = new \DateTime(date('Y-01-01'));
|
||||
$twentiethweek->setISODate($twentiethweek->format('Y'), 20);
|
||||
$startdatetime = $this->change_event_startdate($twentiethweek->format('Ymd\T090000'), 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate($twentiethweek->format('Ymd\T090000'), 'America/New_York');
|
||||
|
||||
$startdate = clone($startdatetime);
|
||||
$startdate->modify($startdate->format('Y-m-d'));
|
||||
@@ -2237,9 +2237,9 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_thursday_in_march_forever(): void {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to the first Thursday of March of the current year at 9am US/Eastern time.
|
||||
// Change our event's date to the first Thursday of March of the current year at 9am America/New_York time.
|
||||
$firstthursdayofmarch = new \DateTime('first Thursday of March');
|
||||
$startdatetime = $this->change_event_startdate($firstthursdayofmarch->format('Ymd\T090000'), 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate($firstthursdayofmarch->format('Ymd\T090000'), 'America/New_York');
|
||||
|
||||
$interval = new \DateInterval('P1Y');
|
||||
|
||||
@@ -2296,9 +2296,9 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_thursday_june_july_august_forever(): void {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to the first Thursday of June in the current year at 9am US/Eastern time.
|
||||
// Change our event's date to the first Thursday of June in the current year at 9am America/New_York time.
|
||||
$firstthursdayofjune = new \DateTime('first Thursday of June');
|
||||
$startdatetime = $this->change_event_startdate($firstthursdayofjune->format('Ymd\T090000'), 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate($firstthursdayofjune->format('Ymd\T090000'), 'America/New_York');
|
||||
|
||||
$startdate = new \DateTime($startdatetime->format('Y-m-d'));
|
||||
|
||||
@@ -2353,8 +2353,8 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_friday_the_thirteenth_forever(): void {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to the first Thursday of June in the current year at 9am US/Eastern time.
|
||||
$this->change_event_startdate(date('Ymd\T090000'), 'US/Eastern');
|
||||
// Change our event's date to the first Thursday of June in the current year at 9am America/New_York time.
|
||||
$this->change_event_startdate(date('Ymd\T090000'), 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2390,10 +2390,10 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_first_saturday_following_first_sunday_forever(): void {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to the next Saturday after the first Sunday of the the current month at 9am US/Eastern time.
|
||||
// Change our event's date to the next Saturday after the first Sunday of the current month at 9am America/New_York time.
|
||||
$firstsaturdayafterfirstsunday = new \DateTime('first Sunday of this month');
|
||||
$firstsaturdayafterfirstsunday->modify('next Saturday');
|
||||
$startdatetime = $this->change_event_startdate($firstsaturdayafterfirstsunday->format('Ymd\T090000'), 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate($firstsaturdayafterfirstsunday->format('Ymd\T090000'), 'America/New_York');
|
||||
$startdate = new \DateTime($startdatetime->format('Y-m-d'));
|
||||
$offset = $startdatetime->diff($startdate, true);
|
||||
|
||||
@@ -2451,7 +2451,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
$electiondate->modify('+1 Tuesday');
|
||||
|
||||
// Use the most recent election date as the starting date of our recurring events.
|
||||
$startdatetime = $this->change_event_startdate($electiondate->format('Ymd\T090000'), 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate($electiondate->format('Ymd\T090000'), 'America/New_York');
|
||||
$startdate = new \DateTime($startdatetime->format('Y-m-d'));
|
||||
$offset = $startdatetime->diff($startdate, true);
|
||||
|
||||
@@ -2494,7 +2494,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_monthly_bysetpos_3_count(): void {
|
||||
global $DB;
|
||||
|
||||
$this->change_event_startdate('19970904T090000', 'US/Eastern');
|
||||
$this->change_event_startdate('19970904T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2531,7 +2531,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_second_to_the_last_weekday_of_the_month(): void {
|
||||
global $DB;
|
||||
|
||||
$this->change_event_startdate('19970929T090000', 'US/Eastern');
|
||||
$this->change_event_startdate('19970929T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2;COUNT=7';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2756,7 +2756,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_weekly_byday_with_wkst_mo(): void {
|
||||
global $DB;
|
||||
|
||||
$this->change_event_startdate('19970805T090000', 'US/Eastern');
|
||||
$this->change_event_startdate('19970805T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2789,7 +2789,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_weekly_byday_with_wkst_su(): void {
|
||||
global $DB;
|
||||
|
||||
$this->change_event_startdate('19970805T090000', 'US/Eastern');
|
||||
$this->change_event_startdate('19970805T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2869,7 +2869,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
* Change the event's timestart (DTSTART) based on the test's needs.
|
||||
*
|
||||
* @param string $datestr The date string. In 'Ymd\This' format. e.g. 19990902T090000.
|
||||
* @param null|string $timezonestr A valid timezone string. e.g. 'US/Eastern'.
|
||||
* @param null|string $timezonestr A valid timezone string. e.g. 'America/New_York'.
|
||||
* If not provided, the default timezone will be used.
|
||||
* @return bool|DateTime
|
||||
*/
|
||||
|
||||
@@ -55,11 +55,11 @@ class cohorts extends system_report {
|
||||
"{$entitymainalias}.component");
|
||||
|
||||
// Check if report needs to show a specific category.
|
||||
$contextid = $this->get_parameter('contextid', 0, PARAM_INT);
|
||||
$showall = $this->get_parameter('showall', true, PARAM_BOOL);
|
||||
if (!$showall) {
|
||||
if (!$this->get_context() instanceof context_system || !$this->get_parameter('showall', false, PARAM_BOOL)) {
|
||||
$paramcontextid = database::generate_param_name();
|
||||
$this->add_base_condition_sql("{$entitymainalias}.contextid = :$paramcontextid", [$paramcontextid => $contextid]);
|
||||
$this->add_base_condition_sql("{$entitymainalias}.contextid = :{$paramcontextid}", [
|
||||
$paramcontextid => $this->get_context()->id,
|
||||
]);
|
||||
}
|
||||
|
||||
// Now we can call our helper methods to add the content we want to include in the report.
|
||||
@@ -77,14 +77,7 @@ class cohorts extends system_report {
|
||||
* @return bool
|
||||
*/
|
||||
protected function can_view(): bool {
|
||||
$contextid = $this->get_parameter('contextid', 0, PARAM_INT);
|
||||
if ($contextid) {
|
||||
$context = context::instance_by_id($contextid, MUST_EXIST);
|
||||
} else {
|
||||
$context = context_system::instance();
|
||||
}
|
||||
|
||||
return has_any_capability(['moodle/cohort:manage', 'moodle/cohort:view'], $context);
|
||||
return has_any_capability(['moodle/cohort:manage', 'moodle/cohort:view'], $this->get_context());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,10 +91,8 @@ class cohorts extends system_report {
|
||||
public function add_columns(cohort $cohortentity): void {
|
||||
|
||||
$entitymainalias = $cohortentity->get_table_alias('cohort');
|
||||
$showall = $this->get_parameter('showall', false, PARAM_BOOL);
|
||||
|
||||
// Category column. An extra callback is appended in order to extend the current column formatting.
|
||||
if ($showall) {
|
||||
if ($this->get_context() instanceof context_system && $this->get_parameter('showall', false, PARAM_BOOL)) {
|
||||
$this->add_column_from_entity('cohort:context')
|
||||
->add_callback(static function(string $value, stdClass $cohort): string {
|
||||
$context = context::instance_by_id($cohort->contextid);
|
||||
@@ -195,10 +186,11 @@ class cohorts extends system_report {
|
||||
*/
|
||||
protected function add_actions(): void {
|
||||
|
||||
$contextid = $this->get_parameter('contextid', 0, PARAM_INT);
|
||||
$showall = $this->get_parameter('showall', true, PARAM_BOOL);
|
||||
$returnurl = (new moodle_url('/cohort/index.php',
|
||||
['id' => ':id', 'contextid' => $contextid, 'showall' => $showall]))->out(false);
|
||||
$returnurl = (new moodle_url('/cohort/index.php', [
|
||||
'id' => ':id',
|
||||
'contextid' => $this->get_context()->id,
|
||||
'showall' => $this->get_parameter('showall', false, PARAM_BOOL),
|
||||
]))->out(false);
|
||||
|
||||
// Hide action. It will be only shown if the property 'visible' is true and user has 'moodle/cohort:manage' capabillity.
|
||||
$this->add_action((new action(
|
||||
|
||||
@@ -405,6 +405,12 @@ class core_cohort_external extends external_api {
|
||||
$results = $results + cohort_get_available_cohorts($context, COHORT_ALL, $limitfrom, $limitnum, $query);
|
||||
}
|
||||
} else if ($includes == 'all') {
|
||||
$contextsystem = context_system::instance();
|
||||
if (!$context instanceof context_system &&
|
||||
!has_any_capability(['moodle/cohort:view', 'moodle/cohort:manage'], $contextsystem)) {
|
||||
|
||||
throw new required_capability_exception($contextsystem, 'moodle/cohort:view', 'nopermissions', '');
|
||||
}
|
||||
$results = cohort_get_all_cohorts($limitfrom, $limitnum, $query);
|
||||
$results = $results['cohorts'];
|
||||
} else {
|
||||
|
||||
+1
-2
@@ -104,8 +104,7 @@ if ($editcontrols = cohort_edit_controls($context, $baseurl)) {
|
||||
echo $OUTPUT->render($editcontrols);
|
||||
}
|
||||
|
||||
$reportparams = ['contextid' => $context->id, 'showall' => $showall];
|
||||
$report = system_report_factory::create(cohorts::class, $context, '', '', 0, $reportparams);
|
||||
$report = system_report_factory::create(cohorts::class, $context, '', '', 0, ['showall' => $showall]);
|
||||
|
||||
// Check if it needs to search by name.
|
||||
if (!empty($searchquery)) {
|
||||
|
||||
@@ -672,6 +672,7 @@ final class externallib_test extends externallib_advanced_testcase {
|
||||
role_assign($userrole, $catuser->id, $catcontext->id);
|
||||
|
||||
// Enrol user in the course.
|
||||
$this->getDataGenerator()->enrol_user($creator->id, $course->id);
|
||||
$this->getDataGenerator()->enrol_user($courseuser->id, $course->id, 'courserole');
|
||||
|
||||
$syscontext = array('contextid' => \context_system::instance()->id);
|
||||
@@ -758,18 +759,27 @@ final class externallib_test extends externallib_advanced_testcase {
|
||||
$this->assertCount(4, $result['cohorts']);
|
||||
|
||||
// A user in the course context with the system cohort:view capability. Check that all the system cohorts are returned.
|
||||
$this->setUser($courseuser);
|
||||
$result = core_cohort_external::search_cohorts("Cohortsearch", $coursecontext, 'all');
|
||||
$this->assertCount(2, $result['cohorts']);
|
||||
$this->assertEquals('Cohortsearch 1', $result['cohorts'][$cohort1->id]->name);
|
||||
$this->assertCount(4, $result['cohorts']);
|
||||
|
||||
// A user in the course context without the ability to view system cohorts.
|
||||
$this->setUser($courseuser);
|
||||
try {
|
||||
$result = core_cohort_external::search_cohorts("Cohortsearch", $coursecontext, 'all');
|
||||
$this->fail('Exception expected');
|
||||
} catch (\Throwable $e) {
|
||||
$this->assertInstanceOf(\required_capability_exception::class, $e);
|
||||
$this->assertStringContainsString('(View site-wide cohorts)', $e->getMessage());
|
||||
}
|
||||
|
||||
// Detect invalid parameter $includes.
|
||||
$this->setUser($creator);
|
||||
try {
|
||||
$result = core_cohort_external::search_cohorts("Cohortsearch", $syscontext, 'invalid');
|
||||
$this->fail('Invalid parameter includes');
|
||||
} catch (\coding_exception $e) {
|
||||
// All good.
|
||||
$this->fail('Exception expected');
|
||||
} catch (\Throwable $e) {
|
||||
$this->assertInstanceOf(\coding_exception::class, $e);
|
||||
$this->assertStringContainsString('Invalid parameter value for \'includes\'', $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,13 @@ class behat_core_competency_generator extends behat_generator_base {
|
||||
'singular' => 'plan',
|
||||
'datagenerator' => 'plan',
|
||||
'required' => ['name'],
|
||||
'switchids' => ['user' => 'userid'],
|
||||
'switchids' => ['user' => 'userid', 'status' => 'status'],
|
||||
],
|
||||
'plan_competencies' => [
|
||||
'singular' => 'plan_competency',
|
||||
'datagenerator' => 'plan_competency',
|
||||
'required' => ['plan', 'competency'],
|
||||
'switchids' => ['competency' => 'competencyid', 'plan' => 'planid'],
|
||||
],
|
||||
'related_competencies' => [
|
||||
'singular' => 'related_competency',
|
||||
@@ -83,14 +89,38 @@ class behat_core_competency_generator extends behat_generator_base {
|
||||
'required' => ['plan', 'competency', 'user'],
|
||||
'switchids' => ['plan' => 'planid', 'competency' => 'competencyid', 'user' => 'userid'],
|
||||
],
|
||||
'user_evidence' => [
|
||||
'singular' => 'user_evidence',
|
||||
'datagenerator' => 'user_evidence',
|
||||
'required' => ['user', 'name'],
|
||||
'switchids' => ['user' => 'userid'],
|
||||
],
|
||||
'user_evidence_competency' => [
|
||||
'singular' => 'user_evidence_competency',
|
||||
'datagenerator' => 'user_evidence_competency',
|
||||
'required' => ['userevidence', 'competency'],
|
||||
'switchids' => ['userevidence' => 'userevidenceid', 'competency' => 'competencyid'],
|
||||
],
|
||||
'templates' => [
|
||||
'singular' => 'template',
|
||||
'datagenerator' => 'template',
|
||||
'required' => ['shortname'],
|
||||
'switchids' => ['context' => 'contextid'],
|
||||
],
|
||||
'template_competencies' => [
|
||||
'singular' => 'template_competency',
|
||||
'datagenerator' => 'template_competency',
|
||||
'required' => ['template', 'competency'],
|
||||
'switchids' => ['template' => 'templateid', 'competency' => 'competencyid'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the competecy framework id using an idnumber.
|
||||
* Get the competency framework id using an idnumber.
|
||||
*
|
||||
* @param string $idnumber
|
||||
* @return int The competecy framework id
|
||||
* @return int The competency framework id
|
||||
*/
|
||||
protected function get_competencyframework_id(string $idnumber): int {
|
||||
global $DB;
|
||||
@@ -103,10 +133,10 @@ class behat_core_competency_generator extends behat_generator_base {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the competecy id using an idnumber.
|
||||
* Get the competency id using an idnumber.
|
||||
*
|
||||
* @param string $idnumber
|
||||
* @return int The competecy id
|
||||
* @return int The competency id
|
||||
*/
|
||||
protected function get_competency_id(string $idnumber): int {
|
||||
global $DB;
|
||||
@@ -135,15 +165,31 @@ class behat_core_competency_generator extends behat_generator_base {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the related competecy id using an idnumber.
|
||||
* Get the related competency id using an idnumber.
|
||||
*
|
||||
* @param string $idnumber
|
||||
* @return int The related competecy id
|
||||
* @return int The related competency id
|
||||
*/
|
||||
protected function get_relatedcompetency_id(string $idnumber): int {
|
||||
return $this->get_competency_id($idnumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template id by shortname.
|
||||
*
|
||||
* @param string $shortname The template name.
|
||||
* @return int
|
||||
*/
|
||||
protected function get_template_id(string $shortname): int {
|
||||
global $DB;
|
||||
|
||||
if (!$id = $DB->get_field('competency_template', 'id', ['shortname' => $shortname])) {
|
||||
throw new Exception('The specified template with name "' . $shortname . '" could not be found.');
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a plan.
|
||||
*
|
||||
@@ -208,6 +254,8 @@ class behat_core_competency_generator extends behat_generator_base {
|
||||
* @return array Processed data.
|
||||
*/
|
||||
protected function preprocess_plan(array $data): array {
|
||||
global $DB, $USER;
|
||||
|
||||
if (isset($data['competencies'])) {
|
||||
$competencies = array_map('trim', str_getcsv($data['competencies']));
|
||||
$data['competencyids'] = array_map([$this, 'get_competency_id'], $competencies);
|
||||
@@ -215,11 +263,20 @@ class behat_core_competency_generator extends behat_generator_base {
|
||||
unset($data['competencies']);
|
||||
}
|
||||
|
||||
global $USER;
|
||||
if (isset($data['reviewer'])) {
|
||||
if (is_number($data['reviewer'])) {
|
||||
$data['reviewerid'] = $data['reviewer'];
|
||||
} else {
|
||||
if (!$userid = $DB->get_field('user', 'id', ['username' => $data['reviewer']])) {
|
||||
throw new Exception('The specified user "' . $data['reviewer'] . '" could not be found.');
|
||||
}
|
||||
$data['reviewerid'] = $userid;
|
||||
}
|
||||
unset($data['reviewer']);
|
||||
}
|
||||
|
||||
return $data + [
|
||||
'userid' => $USER->id,
|
||||
'status' => plan::STATUS_ACTIVE,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -258,4 +315,81 @@ class behat_core_competency_generator extends behat_generator_base {
|
||||
protected function get_data_generator(): core_competency_generator {
|
||||
return $this->componentdatagenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user evidence id using a name.
|
||||
*
|
||||
* @param string $name User evidence name.
|
||||
* @return int The user evidence id
|
||||
*/
|
||||
protected function get_userevidence_id(string $name): int {
|
||||
global $DB;
|
||||
|
||||
if (!$id = $DB->get_field('competency_userevidence', 'id', ['name' => $name])) {
|
||||
throw new Exception('The specified user evidence with name "' . $name . '" could not be found.');
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template competency id using a name.
|
||||
*
|
||||
* @param string $name Template competency name.
|
||||
* @return int The template competency id
|
||||
*/
|
||||
protected function get_templatecompetency_id(string $name): int {
|
||||
global $DB;
|
||||
|
||||
if (!$id = $DB->get_field('competency_template', 'id', ['name' => $name])) {
|
||||
throw new Exception('The specified template competency with name "' . $name . '" could not be found.');
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the context id using a contextid.
|
||||
*
|
||||
* @param string $contextid Context id.
|
||||
* @return int The context id
|
||||
*/
|
||||
protected function get_context_id(string $contextid): int {
|
||||
global $DB;
|
||||
|
||||
if (!$id = $DB->get_field('context', 'id', ['id' => $contextid])) {
|
||||
throw new Exception('The specified context with id "' . $contextid . '" could not be found.');
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status id by status name.
|
||||
*
|
||||
* @param string $name Status name.
|
||||
* @return int
|
||||
*/
|
||||
protected function get_status_id(string $name): int {
|
||||
|
||||
switch ($name) {
|
||||
case 'draft':
|
||||
$status = plan::STATUS_DRAFT;
|
||||
break;
|
||||
case 'in review':
|
||||
$status = plan::STATUS_IN_REVIEW;
|
||||
break;
|
||||
case 'waiting for review':
|
||||
$status = plan::STATUS_WAITING_FOR_REVIEW;
|
||||
break;
|
||||
case 'complete':
|
||||
$status = plan::STATUS_COMPLETE;
|
||||
break;
|
||||
default:
|
||||
$status = plan::STATUS_ACTIVE;
|
||||
break;
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@core @core_contentbank @core_h5p @contenttype_h5p @_switch_iframe @javascript
|
||||
@core @core_contentbank @core_h5p @contenttype_h5p @_switch_iframe
|
||||
Feature: H5P file upload to content bank for admins
|
||||
In order import new H5P content to content bank
|
||||
As an admin
|
||||
@@ -16,6 +16,7 @@ Feature: H5P file upload to content bank for admins
|
||||
And I expand "Site pages" node
|
||||
And I click on "Content bank" "link"
|
||||
|
||||
@javascript
|
||||
Scenario: Admins can upload .h5p extension files to content bank
|
||||
Given I should not see "filltheblanks.h5p"
|
||||
When I click on "Upload" "link"
|
||||
@@ -27,6 +28,7 @@ Feature: H5P file upload to content bank for admins
|
||||
And I wait until the page is ready
|
||||
Then I should see "filltheblanks.h5p"
|
||||
|
||||
@javascript
|
||||
Scenario: Admins can see uploaded H5P contents
|
||||
Given I should not see "filltheblanks.h5p"
|
||||
When I click on "Upload" "link"
|
||||
@@ -40,6 +42,7 @@ Feature: H5P file upload to content bank for admins
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
Then I should see "Of which countries"
|
||||
|
||||
@javascript
|
||||
Scenario: Users can't see content managed by disabled plugins
|
||||
Given I click on "Upload" "link"
|
||||
And I click on "Choose a file..." "button"
|
||||
@@ -55,6 +58,7 @@ Feature: H5P file upload to content bank for admins
|
||||
When I navigate to "Plugins > Content bank" in site administration
|
||||
Then I should not see "filltheblanks.h5p"
|
||||
|
||||
@javascript
|
||||
Scenario: Contents in a context are not available from other contexts
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
@@ -75,6 +79,7 @@ Feature: H5P file upload to content bank for admins
|
||||
And I click on "Content bank" "link"
|
||||
And I should not see "filltheblanks.h5p"
|
||||
|
||||
@javascript
|
||||
Scenario: Admins can upload and deployed content types when libraries are not installed
|
||||
Given I navigate to "H5P > Manage H5P content types" in site administration
|
||||
And I should not see "Fill in the Blanks"
|
||||
@@ -95,6 +100,7 @@ Feature: H5P file upload to content bank for admins
|
||||
And I navigate to "H5P > Manage H5P content types" in site administration
|
||||
And I should see "Fill in the Blanks"
|
||||
|
||||
@javascript
|
||||
Scenario: Uploading invalid packages throws error
|
||||
Given the following "user private files" exist:
|
||||
| user | filepath |
|
||||
@@ -122,3 +128,28 @@ Feature: H5P file upload to content bank for admins
|
||||
And I wait until the page is ready
|
||||
And I should see "It is not possible to unzip it"
|
||||
And I should not see "Sorry, this file is not valid"
|
||||
|
||||
@_file_upload @javascript
|
||||
Scenario: Uploading invalid file types is not allowed
|
||||
Given I am on the "Content bank" page logged in as "admin"
|
||||
When I click on "Upload" "link"
|
||||
And I click on "Choose a file..." "button"
|
||||
And I click on "Upload a file" "link" in the ".fp-repo-area" "css_element"
|
||||
And I set the field "Attachment" to "#dirroot#/course/tests/fixtures/image.jpg"
|
||||
And I press "Upload this file"
|
||||
# Confirm that jpg files cannot be added to content bank.
|
||||
Then "Image (JPEG) filetype cannot be accepted." "text" should exist
|
||||
And I click on "OK" "button" in the "Error" "dialogue"
|
||||
And I click on "Close" "button" in the "File picker" "dialogue"
|
||||
And I click on "Cancel" "button" in the "Upload" "dialogue"
|
||||
# Confirm that jpg file was not added to the content bank.
|
||||
And "No content available" "text" should exist
|
||||
And I should not see "1.jpg"
|
||||
|
||||
Scenario: File upload for content bank can be disabled by admin
|
||||
Given I log in as "admin"
|
||||
And I navigate to "Plugins > Content bank > Manage content types" in site administration
|
||||
When I follow "Disable"
|
||||
And I am on the "Content bank" page
|
||||
# Confirm that "Upload" does not exist after admin disabled from Plugins > Content bank > Manage content types.
|
||||
Then "Upload" "link" should not exist
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
@core @core_contentbank @core_h5p @contentbank_h5p
|
||||
Feature: Content bank contents are retained when course is restored
|
||||
In order to restore content bank contents
|
||||
As a manager
|
||||
I need to be able to restore course containing the content bank
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| admin | C1 | editingteacher |
|
||||
And the following "contentbank content" exist:
|
||||
| contextlevel | reference | contenttype | user | contentname | filepath |
|
||||
| Course | C1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And the following config values are set as admin:
|
||||
| unaddableblocks | | theme_boost|
|
||||
# TODO MDL-57120 site "Content bank" link not accessible without navigation block.
|
||||
And I add the "Navigation" block if not present
|
||||
|
||||
@javascript
|
||||
Scenario: Deleted courses with content banks can be restored
|
||||
Given I navigate to "Courses > Manage courses and categories" in site administration
|
||||
And I click on "delete" action for "Course 1" in management course listing
|
||||
And I click on "Delete" "button" in the "Confirm" "dialogue"
|
||||
And I press "Continue"
|
||||
And I navigate to "Recycle bin" in current page administration
|
||||
And I click on "Restore" "link" in the "Course 1" "table_row"
|
||||
And I am on "Course 1" course homepage
|
||||
And I expand "Site pages" node
|
||||
When I click on "Content bank" "link"
|
||||
And I click on "filltheblanks.h5p" "link"
|
||||
And I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
Then I should see "Of which countries are Berlin, Washington, Beijing, Canberra and Brasilia the capitals?"
|
||||
@@ -272,7 +272,7 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr
|
||||
if (!$coursecat) {
|
||||
// Course category not found.
|
||||
if ($strictness == MUST_EXIST) {
|
||||
throw new moodle_exception('unknowncategory');
|
||||
throw new moodle_exception('unknowncategory', a: $id);
|
||||
}
|
||||
$coursecat = null;
|
||||
} else if (!$alwaysreturnhidden && !$coursecat->is_uservisible($user)) {
|
||||
|
||||
+35
-10
@@ -941,6 +941,8 @@ class core_course_external extends external_api {
|
||||
$params = self::validate_parameters(self::create_courses_parameters(),
|
||||
array('courses' => $courses));
|
||||
|
||||
$courseconfig = get_config('moodlecourse');
|
||||
|
||||
$availablethemes = core_component::get_plugin_list('theme');
|
||||
$availablelangs = get_string_manager()->get_list_of_translations();
|
||||
|
||||
@@ -967,6 +969,14 @@ class core_course_external extends external_api {
|
||||
throw new moodle_exception('errorinvalidparam', 'webservice', '', 'shortname');
|
||||
}
|
||||
|
||||
// Make sure start/end date are correctly set.
|
||||
if (!array_key_exists('startdate', $course)) {
|
||||
$course['startdate'] = usergetmidnight(time());
|
||||
}
|
||||
if (!array_key_exists('enddate', $course) && $courseconfig->courseenddateenabled) {
|
||||
$course['enddate'] = $course['startdate'] + $courseconfig->courseduration;
|
||||
}
|
||||
|
||||
// Make sure lang is valid
|
||||
if (array_key_exists('lang', $course)) {
|
||||
if (empty($availablelangs[$course['lang']])) {
|
||||
@@ -988,6 +998,8 @@ class core_course_external extends external_api {
|
||||
}
|
||||
}
|
||||
|
||||
$course['showactivitydates'] = $courseconfig->showactivitydates;
|
||||
|
||||
//force visibility if ws user doesn't have the permission to set it
|
||||
$category = $DB->get_record('course_categories', array('id' => $course['categoryid']));
|
||||
if (!has_capability('moodle/course:visibility', $context)) {
|
||||
@@ -995,7 +1007,6 @@ class core_course_external extends external_api {
|
||||
}
|
||||
|
||||
//set default value for completion
|
||||
$courseconfig = get_config('moodlecourse');
|
||||
if (completion_info::is_enabled_for_site()) {
|
||||
if (!array_key_exists('enablecompletion', $course)) {
|
||||
$course['enablecompletion'] = $courseconfig->enablecompletion;
|
||||
@@ -4252,7 +4263,7 @@ class core_course_external extends external_api {
|
||||
|
||||
self::validate_context($usercontext);
|
||||
|
||||
if ($userid != $USER->id and !has_capability('moodle/user:viewdetails', $usercontext)) {
|
||||
if ($userid != $USER->id && !has_capability('moodle/user:viewalldetails', $usercontext)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
@@ -4302,22 +4313,28 @@ class core_course_external extends external_api {
|
||||
* @param int $groupid Group id from which the users will be obtained
|
||||
* @param bool $onlyactive Whether to return only the active enrolled users or all enrolled users in the course.
|
||||
* @return array List of users
|
||||
* @throws invalid_parameter_exception
|
||||
*/
|
||||
public static function get_enrolled_users_by_cmid(int $cmid, int $groupid = 0, bool $onlyactive = false) {
|
||||
global $PAGE;
|
||||
global $PAGE;
|
||||
|
||||
$warnings = [];
|
||||
|
||||
self::validate_parameters(self::get_enrolled_users_by_cmid_parameters(), [
|
||||
'cmid' => $cmid,
|
||||
'groupid' => $groupid,
|
||||
'onlyactive' => $onlyactive,
|
||||
[
|
||||
'cmid' => $cmid,
|
||||
'groupid' => $groupid,
|
||||
'onlyactive' => $onlyactive,
|
||||
] = self::validate_parameters(self::get_enrolled_users_by_cmid_parameters(), [
|
||||
'cmid' => $cmid,
|
||||
'groupid' => $groupid,
|
||||
'onlyactive' => $onlyactive,
|
||||
]);
|
||||
|
||||
list($course, $cm) = get_course_and_cm_from_cmid($cmid);
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
self::validate_context($coursecontext);
|
||||
|
||||
course_require_view_participants($coursecontext);
|
||||
|
||||
$enrolledusers = get_enrolled_users($coursecontext, '', $groupid, 'u.*', null, 0, 0, $onlyactive);
|
||||
|
||||
$users = array_map(function ($user) use ($PAGE) {
|
||||
@@ -4596,9 +4613,17 @@ class core_course_external extends external_api {
|
||||
$coursecontext = context_course::instance($courseid);
|
||||
self::validate_context($coursecontext);
|
||||
|
||||
$activeplugin = get_config('core', 'activitychooseractivefooter');
|
||||
// The active plugin must be set, and be present on the site.
|
||||
$activeplugin = clean_param(
|
||||
get_config('core', 'activitychooseractivefooter'),
|
||||
PARAM_COMPONENT,
|
||||
);
|
||||
|
||||
if ($activeplugin !== COURSE_CHOOSER_FOOTER_NONE) {
|
||||
if (
|
||||
$activeplugin !== COURSE_CHOOSER_FOOTER_NONE &&
|
||||
!empty($activeplugin) &&
|
||||
core_component::get_component_directory($activeplugin) !== null
|
||||
) {
|
||||
$footerdata = component_callback($activeplugin, 'custom_chooser_footer', [$courseid, $sectionid]);
|
||||
return [
|
||||
'footer' => true,
|
||||
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user