Compare commits
480 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 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 | |||
| 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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -4327,6 +4327,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>
|
||||
@@ -4522,6 +4523,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>
|
||||
@@ -4714,6 +4716,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>
|
||||
@@ -4893,6 +4896,208 @@
|
||||
</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_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>
|
||||
</COMPATIBILITY_MATRIX>
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -31,19 +31,19 @@ Feature: Manage plan workflow
|
||||
| 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 |
|
||||
@@ -89,18 +89,18 @@ Feature: Manage plan workflow
|
||||
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"
|
||||
@@ -111,8 +111,8 @@ Feature: Manage plan workflow
|
||||
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"
|
||||
@@ -124,8 +124,8 @@ Feature: Manage plan workflow
|
||||
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"
|
||||
@@ -137,8 +137,8 @@ Feature: Manage plan workflow
|
||||
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"
|
||||
@@ -153,8 +153,8 @@ Feature: Manage plan workflow
|
||||
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"
|
||||
@@ -172,8 +172,8 @@ Feature: Manage plan workflow
|
||||
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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
File diff suppressed because one or more lines are too long
@@ -422,8 +422,10 @@ export default class {
|
||||
* @param {array} cmIds the list of course modules ids
|
||||
* @param {bool} complete the new completion value
|
||||
*/
|
||||
cmCompletion(stateManager, cmIds, complete) {
|
||||
async cmCompletion(stateManager, cmIds, complete) {
|
||||
const newState = (complete) ? 1 : 0;
|
||||
const action = (newState == 1) ? 'cm_complete' : 'cm_uncomplete';
|
||||
const logEntry = this._getLoggerEntry(stateManager, action, cmIds);
|
||||
stateManager.setReadOnly(false);
|
||||
cmIds.forEach((id) => {
|
||||
const element = stateManager.get('cm', id);
|
||||
@@ -433,6 +435,7 @@ export default class {
|
||||
}
|
||||
});
|
||||
stateManager.setReadOnly(true);
|
||||
stateManager.addLoggerEntry(await logEntry);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -362,6 +362,9 @@ class stateactions {
|
||||
// We need to get the latest modinfo on each iteration because the section numbers change.
|
||||
$modinfo = get_fast_modinfo($course);
|
||||
$section = $modinfo->get_section_info_by_id($sectionid, MUST_EXIST);
|
||||
if (!course_can_delete_section($course, $section)) {
|
||||
continue;
|
||||
}
|
||||
// Send all activity deletions.
|
||||
if (!empty($modinfo->sections[$section->section])) {
|
||||
foreach ($modinfo->sections[$section->section] as $modnumber) {
|
||||
|
||||
@@ -150,47 +150,3 @@ Feature: Course index completion icons
|
||||
| 1 | False |
|
||||
When I am on the "C1" "Course" page logged in as "student1"
|
||||
And "Done" "icon" should exist in the "courseindex-content" "region"
|
||||
|
||||
@javascript
|
||||
Scenario: Activities with custom completion rules could fail
|
||||
Given the following "activity" exists:
|
||||
| activity | scorm |
|
||||
| course | C1 |
|
||||
| name | Music history |
|
||||
| packagefilepath | mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12-mini.zip |
|
||||
| maxattempt | 1 |
|
||||
| latattemptlock | 1 |
|
||||
# Add requirements
|
||||
| completion | 2 |
|
||||
| completionscorerequired | 90 |
|
||||
Given I am on the "Music history" "scorm activity" page logged in as student1
|
||||
# We need a little taller window because Firefox is, apparently, unable to auto-scroll within
|
||||
# an iframe, so we need to ensure that the "Save changes" button is visible in the viewport.
|
||||
And I change window size to "large"
|
||||
And I press "Enter"
|
||||
And I switch to the main frame
|
||||
And I click on "Par?" "list_item"
|
||||
And I switch to "scorm_object" iframe
|
||||
And I wait until the page is ready
|
||||
And I switch to the main frame
|
||||
And I click on "Keeping Score" "list_item"
|
||||
And I switch to "scorm_object" iframe
|
||||
And I wait until the page is ready
|
||||
And I switch to the main frame
|
||||
And I click on "Other Scoring Systems" "list_item"
|
||||
And I switch to "scorm_object" iframe
|
||||
And I wait until the page is ready
|
||||
And I switch to the main frame
|
||||
And I click on "The Rules of Golf" "list_item"
|
||||
And I switch to "scorm_object" iframe
|
||||
And I wait until the page is ready
|
||||
And I switch to the main frame
|
||||
And I click on "Playing Golf Quiz" "list_item"
|
||||
And I switch to "scorm_object" iframe
|
||||
And I wait until the page is ready
|
||||
And I click on "[id='question_com.scorm.golfsamples.interactions.playing_1_1']" "css_element"
|
||||
And I press "Submit Answers"
|
||||
And I wait until "Score: 20" "text" exists
|
||||
And I switch to the main frame
|
||||
And I click on "Exit activity" "link"
|
||||
And "Failed" "icon" should exist in the "courseindex-content" "region"
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<?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/>.
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace core_courseformat\external;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use core_courseformat\stateactions;
|
||||
use core_courseformat\stateupdates;
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
|
||||
|
||||
/**
|
||||
* Tests for the delete section test class.
|
||||
*
|
||||
* @package core_courseformat
|
||||
* @copyright 2025 Laurent David <laurent.david@moodle.com>
|
||||
* @category test
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @coversDefaultClass \core_courseformat\stateactions
|
||||
*/
|
||||
final class delete_section_test extends \externallib_advanced_testcase {
|
||||
|
||||
/**
|
||||
* Setup to ensure that fixtures are loaded.
|
||||
*/
|
||||
public static function setupBeforeClass(): void { // phpcs:ignore
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittestdelete.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the webservice can execute the section_delete action.
|
||||
*
|
||||
* @covers ::section_delete
|
||||
* @dataProvider section_delete_provider
|
||||
* @param int $sectionum
|
||||
* @param string $format
|
||||
* @param array $formatoptions
|
||||
* @param int $expectedsectionum
|
||||
*
|
||||
* @throws \moodle_exception
|
||||
*/
|
||||
public function test_delete_section(int $sectionum, string $format, array $formatoptions, int $expectedsectionum): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$course =
|
||||
$this->getDataGenerator()->create_course(array_merge(
|
||||
['numsections' => $sectionum, 'format' => $format],
|
||||
$formatoptions,
|
||||
));
|
||||
$teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
|
||||
// Execute the method.
|
||||
$courseformat = course_get_format($course->id);
|
||||
$updates = new stateupdates($courseformat);
|
||||
$modinfo = get_fast_modinfo($course);
|
||||
$sections = $modinfo->get_section_info_all();
|
||||
$sectionsid = array_map(function ($section) {
|
||||
return $section->id;
|
||||
}, $sections);
|
||||
$actions = new stateactions();
|
||||
$this->setUser($teacher);
|
||||
$actions->section_delete(
|
||||
$updates,
|
||||
$course,
|
||||
$sectionsid
|
||||
);
|
||||
// Check result.
|
||||
$modinfo = get_fast_modinfo($course);
|
||||
$sections = $modinfo->get_section_info_all();
|
||||
$this->assertCount($expectedsectionum, $sections);
|
||||
if ($format == 'theunittestdelete') {
|
||||
$this->assertDebuggingCalled();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for the test_delete_section method.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function section_delete_provider(): array {
|
||||
return [
|
||||
'format topic' => [
|
||||
'sectionum' => 4,
|
||||
'format' => 'topics',
|
||||
'formatoptions' => [],
|
||||
'expectedsectionum' => 1,
|
||||
],
|
||||
'format theunittestdelete' => [
|
||||
'sectionum' => 4,
|
||||
'format' => 'theunittestdelete',
|
||||
'formatoptions' => [],
|
||||
'expectedsectionum' => 5,
|
||||
],
|
||||
'format theunittestdelete can delete' => [
|
||||
'sectionum' => 4,
|
||||
'format' => 'theunittestdelete',
|
||||
'formatoptions' => ['can_delete_sections' => true],
|
||||
'expectedsectionum' => 1,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?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/>.
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
require_once(__DIR__ . '/format_theunittest.php');
|
||||
|
||||
/**
|
||||
* Fixture for fake course format testing course format API.
|
||||
*
|
||||
* @package core_courseformat
|
||||
* @copyright 2025 Laurent David <laurent.david@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class format_theunittestdelete extends format_theunittest {
|
||||
|
||||
/**
|
||||
* Definitions of the additional options that format uses
|
||||
*
|
||||
* @param bool $foreditform
|
||||
* @return array of options
|
||||
*/
|
||||
public function course_format_options($foreditform = false) {
|
||||
static $courseformatoptions = false;
|
||||
if ($courseformatoptions === false) {
|
||||
$courseformatoptions = parent::course_format_options(true);
|
||||
$courseformatoptionsadditional = [
|
||||
'can_delete_sections' => [
|
||||
'default' => false,
|
||||
'type' => PARAM_BOOL,
|
||||
],
|
||||
];
|
||||
$courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsadditional);
|
||||
}
|
||||
return $courseformatoptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this format allows to delete sections
|
||||
*
|
||||
* Here for test purpose we just can delete one section every two sections
|
||||
*
|
||||
* Do not call this function directly, instead use course_can_delete_section()
|
||||
*
|
||||
* @param int|stdClass|section_info $section
|
||||
* @return bool
|
||||
*/
|
||||
public function can_delete_section($section) {
|
||||
return $this->get_format_options()['can_delete_sections'] ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this course format uses sections
|
||||
*/
|
||||
public function uses_sections() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -1156,7 +1156,14 @@ class core_course_renderer extends plugin_renderer_base {
|
||||
// This is a request for the course information.
|
||||
$courseid = required_param('courseid', PARAM_INT);
|
||||
|
||||
$course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
|
||||
$course = $DB->get_record('course', ['id' => $courseid], '*', IGNORE_MISSING);
|
||||
if ($course === false) {
|
||||
throw new \moodle_exception('invalidcourseid');
|
||||
}
|
||||
$coursecontext = context_course::instance($course->id, MUST_EXIST);
|
||||
if ($course->visible == 0 && !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
|
||||
throw new \moodle_exception('invalidcourseid');
|
||||
}
|
||||
|
||||
$chelper = new coursecat_helper();
|
||||
$chelper->set_show_courses(self::COURSECAT_SHOW_COURSES_EXPANDED);
|
||||
|
||||
@@ -186,7 +186,7 @@ class course_reset_form extends moodleform {
|
||||
$coursedata['enddate'] = $data['reset_end_date'];
|
||||
} else if ($data['reset_start_date'] > 0 && $course->enddate != 0) {
|
||||
// Otherwise, if the current course enddate is set, reset_course_userdata will add the start date time shift to it.
|
||||
$timeshift = $data['reset_start_date'] - usergetmidnight($course->startdate);
|
||||
$timeshift = $data['reset_start_date'] - $course->startdate;
|
||||
$coursedata['enddate'] = $course->enddate + $timeshift;
|
||||
} else {
|
||||
$coursedata['enddate'] = $course->enddate;
|
||||
|
||||
@@ -3696,7 +3696,16 @@ final class courselib_test extends advanced_testcase {
|
||||
$time + YEARSECS,
|
||||
$time + WEEKSECS,
|
||||
$time + YEARSECS
|
||||
]
|
||||
],
|
||||
// Time shift is between exact times, not midnight(s) (MDL-65233).
|
||||
[
|
||||
$time + HOURSECS,
|
||||
$time + DAYSECS,
|
||||
$time + WEEKSECS + HOURSECS,
|
||||
false,
|
||||
$time + WEEKSECS + HOURSECS,
|
||||
$time + WEEKSECS + DAYSECS,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -3850,6 +3850,12 @@ final class externallib_test extends externallib_advanced_testcase {
|
||||
$this->assignUserCapability('moodle/user:viewdetails', $usercontext, $teacherroleid);
|
||||
|
||||
// Sorted by course id DESC.
|
||||
// User without moodle/user:viewalldetails capability will not be able to see the course details.
|
||||
$result = core_course_external::get_recent_courses($student->id);
|
||||
$this->assertCount(0, $result);
|
||||
|
||||
// User with moodle/user:viewalldetails capability will be able to see the course details.
|
||||
$this->assignUserCapability('moodle/user:viewalldetails', $usercontext, $teacherroleid);
|
||||
$result = core_course_external::get_recent_courses($student->id);
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertEquals($courses[0]->id, array_shift($result)->id);
|
||||
@@ -3953,6 +3959,12 @@ final class externallib_test extends externallib_advanced_testcase {
|
||||
|
||||
$this->assertEquals(2, count($users['users']));
|
||||
$this->assertEquals($expectedusers, $users);
|
||||
|
||||
// Prohibit the capability for viewing course participants.
|
||||
$this->unassignUserCapability('moodle/course:viewparticipants', null, null, $course1->id);
|
||||
$this->expectException(required_capability_exception::class);
|
||||
$this->expectExceptionMessage('Sorry, but you do not currently have permissions to do that (View participants)');
|
||||
core_course_external::get_enrolled_users_by_cmid($forum1->cmid);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+10
-1
@@ -84,11 +84,20 @@ function filter_tex_sanitize_formula(string $texexp): string {
|
||||
'\afterassignment', '\expandafter', '\noexpand', '\special',
|
||||
'\let', '\futurelet', '\else', '\fi', '\chardef', '\makeatletter', '\afterground',
|
||||
'\noexpand', '\line', '\mathcode', '\item', '\section', '\mbox', '\declarerobustcommand',
|
||||
'\ExplSyntaxOn', '\pdffiledump',
|
||||
'\ExplSyntaxOn', '\pdffiledump', '\mathtex',
|
||||
];
|
||||
|
||||
$allowlist = ['inputenc'];
|
||||
|
||||
// Add encoded backslash (\) versions of backslashed items to deny list.
|
||||
$encodedslashdenylist = array_map(function($value) {
|
||||
$encoded = str_replace('\\', '\', $value);
|
||||
// Return an encoded slash version if a slash is found, otherwise null so we can filter it off.
|
||||
return $encoded != $value ? $encoded : null;
|
||||
}, $denylist);
|
||||
$encodedslashdenylist = array_filter($encodedslashdenylist);
|
||||
$denylist = array_merge($denylist, $encodedslashdenylist);
|
||||
|
||||
// Prepare the denylist for regular expression.
|
||||
$denylist = array_map(function($value){
|
||||
return '/' . preg_quote($value, '/') . '/i';
|
||||
|
||||
+1
-1
@@ -203,7 +203,7 @@ class fetch extends external_api {
|
||||
$definitionid,
|
||||
'remark',
|
||||
$filling['remark'],
|
||||
(int) $filling['remarkformat']
|
||||
(int) FORMAT_HTML
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -857,16 +857,19 @@ class gradingform_guide_instance extends gradingform_instance {
|
||||
parent::update($data);
|
||||
|
||||
foreach ($data['criteria'] as $criterionid => $record) {
|
||||
// Hardcoding/defaulting to html format for new/existing record
|
||||
$record['remarkformat'] = FORMAT_HTML;
|
||||
|
||||
if (!array_key_exists($criterionid, $currentgrade['criteria'])) {
|
||||
$newrecord = array('instanceid' => $this->get_id(), 'criterionid' => $criterionid,
|
||||
'score' => $record['score'], 'remarkformat' => FORMAT_MOODLE);
|
||||
'score' => $record['score'], 'remarkformat' => $record['remarkformat']);
|
||||
if (isset($record['remark'])) {
|
||||
$newrecord['remark'] = $record['remark'];
|
||||
}
|
||||
$DB->insert_record('gradingform_guide_fillings', $newrecord);
|
||||
} else {
|
||||
$newrecord = array('id' => $currentgrade['criteria'][$criterionid]['id']);
|
||||
foreach (array('score', 'remark'/*, 'remarkformat' TODO */) as $key) {
|
||||
foreach (array('score', 'remark', 'remarkformat') as $key) {
|
||||
if (isset($record[$key]) && $currentgrade['criteria'][$criterionid][$key] != $record[$key]) {
|
||||
$newrecord[$key] = $record[$key];
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ Feature: Teacher can edit a marking guide state
|
||||
| activity | course | name | advancedgradingmethod_submissions |
|
||||
| assign | C1 | Assign 1 | guide |
|
||||
|
||||
Scenario: Marking guide state can be changed to draft
|
||||
Scenario Outline: Marking guide state can be changed
|
||||
Given I am on the "Course 1" course page logged in as teacher1
|
||||
And I go to "Assign 1" advanced grading definition page
|
||||
And I set the following fields to these values:
|
||||
@@ -28,12 +28,12 @@ Feature: Teacher can edit a marking guide state
|
||||
| Criterion name | Description for students | Description for markers | Maximum score |
|
||||
| Grade Criteria 1 | Grade 1 description for students | Grade 1 description for markers | 70 |
|
||||
| Grade Criteria 2 | Grade 2 description for students | Grade 2 description for markers | 30 |
|
||||
And I press "Save marking guide and make it ready"
|
||||
And I should not see "Please note: the advanced grading form is not ready at the moment. Simple grading method will be used until the form has a valid status."
|
||||
And I should not see "Assign 1 marking guide Draft"
|
||||
And I should see "Assign 1 marking guide Ready for use"
|
||||
And I click on "Edit the current form definition" "link"
|
||||
When I press "Save as draft"
|
||||
Then I should see "Please note: the advanced grading form is not ready at the moment. Simple grading method will be used until the form has a valid status."
|
||||
And I should see "Assign 1 marking guide Draft"
|
||||
And I should not see "Assign 1 marking guide Ready for use"
|
||||
When I press "<buttonname>"
|
||||
Then I <drafttextvisibility> see "Please note: the advanced grading form is not ready at the moment. Simple grading method will be used until the form has a valid status."
|
||||
And I <drafttextvisibility> see "Assign 1 marking guide Draft"
|
||||
And I <readytextvisibility> see "Assign 1 marking guide Ready for use"
|
||||
|
||||
Examples:
|
||||
| buttonname | drafttextvisibility | readytextvisibility |
|
||||
| Save marking guide and make it ready | should not | should |
|
||||
| Save as draft | should | should not |
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
@gradingform @gradingform_guide
|
||||
Feature: Marking guide can handle maximum grade mismatches
|
||||
In order to handle maximum grade mismatches
|
||||
As a teacher
|
||||
I should be able to set the maximum grade
|
||||
|
||||
Background:
|
||||
Given the following "user" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
|
||||
Scenario Outline: Marking guide maximum grade handling
|
||||
Given the following "activities" exist:
|
||||
| activity | course | name | advancedgradingmethod_submissions |
|
||||
| assign | C1 | Assign 1 | guide |
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
And I go to "Assign 1" advanced grading definition page
|
||||
And I set the following fields to these values:
|
||||
| Name | Assign 1 marking guide |
|
||||
| Description | Marking guide description |
|
||||
And I define the following marking guide:
|
||||
| Criterion name | Description for students | Description for markers | Maximum score |
|
||||
| Grade Criteria 1 | Grade 1 description for students | Grade 1 description for markers | <maxscore> |
|
||||
| Grade Criteria 2 | Grade 2 description for students | Grade 2 description for markers | 30 |
|
||||
When I press "Save marking guide and make it ready"
|
||||
Then I should see "Assign 1 marking guide Ready for use"
|
||||
# Please note: We need to add the no-break space unicode character to the warning message otherwise it will fail.
|
||||
And I should see "WARNING: Your marking guide has a maximum grade of <totalmaxscore> points but the maximum grade set in your activity is 100 The maximum score set in your marking guide will be scaled to the maximum grade in the module."
|
||||
And I should see "Intermediate scores will be converted respectively and rounded to the nearest available grade."
|
||||
|
||||
Examples:
|
||||
# <totalmaxscore> value is derived from <maxscore> + maximum score assigned to criteria 2 (30).
|
||||
# Case 1: total > max score of 100.
|
||||
# Case 2: total < max score of 100.
|
||||
| maxscore | totalmaxscore |
|
||||
| 90 | 120 |
|
||||
| 50 | 80 |
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user