Compare commits
365 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a38a37f1f | |||
| 4ea1bd3537 | |||
| 35b7cbf027 | |||
| 87eb0fdf21 | |||
| c8aef2e4ba | |||
| fa72ebc155 | |||
| 6d7f162df0 | |||
| 97d64fb809 | |||
| fd3c586992 | |||
| 61fb8ee988 | |||
| 44f8c11123 | |||
| e5b9510af2 | |||
| ab151f86ab | |||
| e36e40f530 | |||
| 3a8a8c8463 | |||
| 2f811e6acf | |||
| fb7da9c74f | |||
| d27862e2ca | |||
| c7fde934c7 | |||
| be50fbe20c | |||
| 74bf7382dd | |||
| cb075f6257 | |||
| 8a1b5fc2b0 | |||
| bd22abe778 | |||
| 80083244b6 | |||
| 560f4d895b | |||
| 95c3017da4 | |||
| 8fffc08ee8 | |||
| 364230d6f8 | |||
| 45f674e8cb | |||
| 4ba166cf80 | |||
| 3a779663aa | |||
| ca7b24f6d0 | |||
| 35a2a34a7d | |||
| 603c794190 | |||
| 38c4d51947 | |||
| a86228f547 | |||
| b30033c6c0 | |||
| 5661849a02 | |||
| 66e8ce9976 | |||
| cf92eaeb0b | |||
| f7f40e54e6 | |||
| 627177855a | |||
| 8c7983a43f | |||
| 0d0aa11e19 | |||
| eb0439b5da | |||
| 37101f0698 | |||
| 4b8a001bb0 | |||
| a9cf9d5519 | |||
| 53275e5b8f | |||
| 518e5799e4 | |||
| 6a610605f6 | |||
| e4eb16bd9f | |||
| 3e831cc8c5 | |||
| 5ecca13a6f | |||
| 278f58dae0 | |||
| 73cf6fb65c | |||
| 6483b5d68f | |||
| 6c24b8289f | |||
| e2e00a9dd4 | |||
| 8168155e17 | |||
| d6650b63bf | |||
| 174389cc26 | |||
| 03454c2262 | |||
| e591ddc4e8 | |||
| e1767fa80f | |||
| e25033e42d | |||
| dd2c19eb09 | |||
| 3b8b8d7d84 | |||
| 5378285a9a | |||
| 3a098250f1 | |||
| 148ddd26e7 | |||
| cdf432ee17 | |||
| b1e918edd8 | |||
| f58df6fa88 | |||
| eae562817d | |||
| 033a59f9c5 | |||
| a34a8636be | |||
| 08d2e7c136 | |||
| 886bbcbdb8 | |||
| 0c3e759d98 | |||
| 607a452a74 | |||
| 3ef6e04f3c | |||
| dfc7dcd062 | |||
| 80d773868c | |||
| eb127cc93b | |||
| 86716dc120 | |||
| e5eb66e405 | |||
| 3a1b615718 | |||
| 54a8516cd2 | |||
| 628e7ecc4c | |||
| 7762fc61e8 | |||
| 3ffafd9232 | |||
| 7c82e1206d | |||
| 0152978745 | |||
| 2302bb7e2b | |||
| d1ccb8702c | |||
| 4cb1509e35 | |||
| f764c332e7 | |||
| 67f869a081 | |||
| 467a28d921 | |||
| 3e4c1c959f | |||
| 69d235426a | |||
| bbb8622464 | |||
| 7311dff041 | |||
| 8f2f95bd28 | |||
| 579a5061ef | |||
| a11a9ca5aa | |||
| 2ff0e12b24 | |||
| 3f163e8330 | |||
| f5ada5cb0b | |||
| 63b2ceee5f | |||
| ee9d740ccf | |||
| 13c201fbc7 | |||
| b9fd956e73 | |||
| 2f216f5bdb | |||
| fcebf9b8a6 | |||
| 7ace11d396 | |||
| ad377e3085 | |||
| 9b65beb7ed | |||
| 4afd2390e2 | |||
| 184aa8d8ca | |||
| 9e1916ba09 | |||
| 7d01008384 | |||
| 4ca230b174 | |||
| 07e0b64822 | |||
| 9acc242e13 | |||
| fbdd721f0b | |||
| 66ae31ecb5 | |||
| 43b30054ef | |||
| 066edbb11b | |||
| 0076a8fe5c | |||
| 534cf6e3d4 | |||
| 771f183646 | |||
| 348efc1495 | |||
| 7b9191508f | |||
| 67f08a792f | |||
| 36a5a2d4d1 | |||
| 52818283fe | |||
| 053aa290b2 | |||
| 1b1a661a03 | |||
| ad3e6363f1 | |||
| 2627f5ba2c | |||
| 223948bf93 | |||
| 6b5485a0b0 | |||
| b16e808764 | |||
| 76ff7600e4 | |||
| a2ddc55f69 | |||
| cc6b3c161f | |||
| 18e5768332 | |||
| fe3ff7dd69 | |||
| 816fadfcd7 | |||
| b136e428d0 | |||
| f02437f915 | |||
| de6c7d2975 | |||
| 7fe281cc45 | |||
| a410d35ef4 | |||
| f16b42d013 | |||
| c26e73fc9f | |||
| 752c0c77f8 | |||
| b49d7c974f | |||
| 1773a1926d | |||
| 0d056137bb | |||
| 6822ab321b | |||
| d8f055240a | |||
| 7527ca4c49 | |||
| e6023839c9 | |||
| 2cf4f88b52 | |||
| 08be365c2c | |||
| 5c13475743 | |||
| a2524d8caf | |||
| aec53e3547 | |||
| a272c2289f | |||
| 055d5cb3b0 | |||
| 3d7b5d51b9 | |||
| 6109f4dfd4 | |||
| 57791ecd61 | |||
| 3f83ea2456 | |||
| 471838be7b | |||
| dfb2be423b | |||
| b554580520 | |||
| b1bb5671df | |||
| cd5d14d73b | |||
| 3fe8dbd027 | |||
| 98a58ef44c | |||
| 95646071ae | |||
| 29d68c07af | |||
| 773bfa9272 | |||
| 2995721139 | |||
| 6488fc6a20 | |||
| 8c678d2b9a | |||
| cedb8a92cb | |||
| 402153c46e | |||
| 8337ef3be5 | |||
| ecf396842f | |||
| bde37edf3c | |||
| 6aeecbdf46 | |||
| b7ebcf7e2c | |||
| 47a38fd827 | |||
| 3089485e57 | |||
| 15a9f6f119 | |||
| dbd62ed4ea | |||
| 2dc6ab3567 | |||
| 8e31874a3a | |||
| f48edc0cd1 | |||
| b2bfd0dcad | |||
| 94ad185d09 | |||
| f0dae38957 | |||
| 9390787a34 | |||
| e302d0d902 | |||
| 2f04edf37f | |||
| ded87e8d92 | |||
| 8ddb933f5c | |||
| d09a390a05 | |||
| e31d7bdb81 | |||
| 246ad70ba9 | |||
| 3f40af87e4 | |||
| a4a654348d | |||
| ed48b54542 | |||
| d8dbfe5d25 | |||
| 9dd88a4988 | |||
| 358386e4f4 | |||
| 6724e13007 | |||
| 56276d997f | |||
| ca345f8853 | |||
| b372929c9b | |||
| 559db765b3 | |||
| 6179d2ba24 | |||
| ba18db3c7c | |||
| 61b5850a8c | |||
| 24f85416a7 | |||
| 69ce4b683d | |||
| f7ea9dd9cc | |||
| bce9cfdfd5 | |||
| 9ed81d3a8a | |||
| 077465cf90 | |||
| c7b9f2980e | |||
| 2524184384 | |||
| 0a6b4bb15c | |||
| 99f9513b05 | |||
| 8974392cfa | |||
| 8ad9114050 | |||
| 5bda7cac35 | |||
| 13ef307253 | |||
| 0577c54bc0 | |||
| cac02a15b6 | |||
| a979e0ce97 | |||
| cad097a605 | |||
| 40b918348c | |||
| 1184d3c20f | |||
| 711a9c0df5 | |||
| e9b513d561 | |||
| d8e0799023 | |||
| 33fa895d25 | |||
| 93ce6e33d9 | |||
| 4018e95a55 | |||
| 9667e96d89 | |||
| a919969ae7 | |||
| 71f3a87583 | |||
| a09ecad620 | |||
| e0d1d8e469 | |||
| d3070236ac | |||
| 8d77dd09b8 | |||
| 1ea9832c3e | |||
| 9eee64d0d5 | |||
| 46f1b71c20 | |||
| 2ee8296efc | |||
| e2a7040d94 | |||
| 06798fb898 | |||
| 8737f16faa | |||
| 776319ac8e | |||
| 76f66df235 | |||
| 4b302063c3 | |||
| ad4736b5ec | |||
| 6637197d97 | |||
| dd115e572a | |||
| 663eed6780 | |||
| 883f98a3f2 | |||
| 0562c4d552 | |||
| 15409cfa41 | |||
| 116eef7a25 | |||
| 686d9cfd4c | |||
| dec259a249 | |||
| f92753a3d8 | |||
| 3d5fa73133 | |||
| e4989b529a | |||
| f7b2fb473b | |||
| dc7da1a416 | |||
| c819926529 | |||
| d20f1024d8 | |||
| 07c200438c | |||
| e146a9d8cd | |||
| ef964137ce | |||
| 650474c0a7 | |||
| 6bc193134a | |||
| 9dbbd31076 | |||
| 8317aa9b32 | |||
| 1944b58bd6 | |||
| 512670fe94 | |||
| fc93938f23 | |||
| 7d158f18d9 | |||
| c22b7c2f82 | |||
| 8fa9f27c7e | |||
| 91e083981d | |||
| 06ebac56c6 | |||
| 9d9e03ef10 | |||
| 05fdc29294 | |||
| 38708efb50 | |||
| 39ffe174e7 | |||
| fa23ae702c | |||
| 2d9a6fa760 | |||
| 1652b5281e | |||
| d8e3c31b32 | |||
| a985ac2e0c | |||
| ec5d7c3227 | |||
| ccae21a639 | |||
| 345d483ee2 | |||
| 4d9b533fd4 | |||
| 174fc341d6 | |||
| bcbab5517b | |||
| 0848bf8687 | |||
| 11e0579540 | |||
| cc9a537bd2 | |||
| 38909cd64e | |||
| 62aa37f70d | |||
| f6e7eb2ebd | |||
| c0fefd0a4d | |||
| b5599f33d0 | |||
| 5e1588ba63 | |||
| 50618adb13 | |||
| 1c023e7fbb | |||
| c6d8aca226 | |||
| c4c514ee46 | |||
| 6c956d3408 | |||
| 052e646cdc | |||
| f1166fe9e6 | |||
| 0d45970bd3 | |||
| d160890cab | |||
| d6fdd2f5f9 | |||
| d188d58ce9 | |||
| 1f602aabec | |||
| 0257e1696f | |||
| 891b6f796c | |||
| 9fbdf21341 | |||
| 7e39cee0f6 | |||
| 82488f901d | |||
| 7744cf2f28 | |||
| e7263580b9 | |||
| a74443a342 | |||
| 7ff2d18b93 | |||
| 66ff0a871f | |||
| 11fe89c08e | |||
| 76bd425474 | |||
| aeeaea6840 | |||
| 5c30074d99 | |||
| 8e53ef0162 | |||
| 2a2d3c85bb | |||
| 805577664b | |||
| cf9373c4dd | |||
| 52ada82da2 | |||
| fca2456e1f | |||
| 848fd484d5 | |||
| 0b28c6a254 | |||
| b8f45d20fd | |||
| e7817d0d55 |
@@ -7,6 +7,12 @@ on:
|
||||
- MOODLE_[0-9]+_STABLE
|
||||
tags-ignore:
|
||||
- v[0-9]+.[0-9]+.[0-9]+*
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
phpunit_extra_options:
|
||||
description: Additional options to apply to PHPUnit
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
env:
|
||||
php: 8.1
|
||||
@@ -17,10 +23,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checking out code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Configuring node & npm
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
@@ -97,7 +103,7 @@ jobs:
|
||||
coverage: none
|
||||
|
||||
- name: Checking out code from ${{ env.GITHUB_REF_SLUG }}
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setting up PHPUnit
|
||||
env:
|
||||
@@ -113,4 +119,4 @@ jobs:
|
||||
env:
|
||||
dbtype: ${{ matrix.db }}
|
||||
phpunit_options: ${{ secrets.phpunit_options }}
|
||||
run: vendor/bin/phpunit $phpunit_options
|
||||
run: vendor/bin/phpunit $phpunit_options ${{ inputs.phpunit_extra_options }}
|
||||
|
||||
@@ -16,10 +16,10 @@ jobs:
|
||||
git config --global core.eol lf
|
||||
|
||||
- name: Checking out code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Configuring node & npm
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
git config --global core.eol lf
|
||||
|
||||
- name: Checking out code from ${{ env.GITHUB_REF_SLUG }}
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Needs to be done after php is available, git configured and Moodle checkout has happened.
|
||||
- name: Setting up moodle-exttests service
|
||||
|
||||
@@ -80,8 +80,8 @@ if (!$category = $DB->get_record('course_categories', ['id' => $options['categor
|
||||
throw new \moodle_exception('invalidcategoryid');
|
||||
}
|
||||
|
||||
$backupdir = "restore_" . uniqid();
|
||||
$path = $CFG->tempdir . DIRECTORY_SEPARATOR . "backup" . DIRECTORY_SEPARATOR . $backupdir;
|
||||
$backupdir = restore_controller::get_tempdir_name(SITEID, $USER->id);
|
||||
$path = make_backup_temp_directory($backupdir);
|
||||
|
||||
cli_heading(get_string('extractingbackupfileto', 'backup', $path));
|
||||
$fp = get_file_packer('application/vnd.moodle.backup');
|
||||
|
||||
@@ -4305,4 +4305,195 @@
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="4.4" requires="4.1.2">
|
||||
<UNICODE level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unicoderequired" />
|
||||
</FEEDBACK>
|
||||
</UNICODE>
|
||||
<DATABASE level="required">
|
||||
<VENDOR name="mariadb" version="10.6.7" />
|
||||
<VENDOR name="mysql" version="8.0" />
|
||||
<VENDOR name="postgres" version="13" />
|
||||
<VENDOR name="mssql" version="14.0" />
|
||||
<VENDOR name="oracle" version="19" />
|
||||
</DATABASE>
|
||||
<PHP version="8.1.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_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_CHECKS>
|
||||
</MOODLE>
|
||||
</COMPATIBILITY_MATRIX>
|
||||
|
||||
@@ -76,7 +76,7 @@ switch ($action) {
|
||||
$optionsyes = array('action'=>'delete', 'roleid'=>$roleid, 'sesskey'=>sesskey(), 'confirm'=>1);
|
||||
$a = new stdClass();
|
||||
$a->id = $roleid;
|
||||
$a->name = $roles[$roleid]->name;
|
||||
$a->name = $roles[$roleid]->localname;
|
||||
$a->shortname = $roles[$roleid]->shortname;
|
||||
$a->count = $DB->count_records_select('role_assignments',
|
||||
'roleid = ?', array($roleid), 'COUNT(DISTINCT userid)');
|
||||
|
||||
@@ -23,12 +23,15 @@
|
||||
* name - form element name
|
||||
* hasoptions - there are some options
|
||||
* options - list of options containing name, key, id, checked, label
|
||||
* readonly - bool
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"name": "test",
|
||||
"hasoptions": true,
|
||||
"options": [ { "name": "Option", "key": "k1", "id": "id0", "checked": true, "label": "Option label"} ]
|
||||
"options": [ { "name": "Option", "key": "k1", "id": "id0", "checked": true, "label": "Option label"} ],
|
||||
"readonly": false
|
||||
|
||||
}
|
||||
}}
|
||||
<div class="form-multicheckbox">
|
||||
@@ -37,7 +40,7 @@
|
||||
<ul>
|
||||
{{#options}}
|
||||
<li>
|
||||
<input type="checkbox" name="{{name}}[{{key}}]" value="1" id="{{id}}_{{key}}" {{#checked}}checked{{/checked}}>
|
||||
<input type="checkbox" name="{{name}}[{{key}}]" value="1" id="{{id}}_{{key}}" {{#readonly}}disabled{{/readonly}} {{#checked}}checked{{/checked}}>
|
||||
<label for="{{id}}_{{key}}">{{{label}}}</label>
|
||||
</li>
|
||||
{{/options}}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
@tool_behat
|
||||
Feature: Confirm that we can open multiple browser tabs
|
||||
In order to use multiple browser tabs
|
||||
As a test writer
|
||||
I need the relevant Behat steps to work
|
||||
|
||||
@javascript @_switch_window
|
||||
Scenario: Open multiple browser tabs
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
| Course 2 | C2 |
|
||||
| Course 3 | C3 |
|
||||
And I am on the "C1" "Course" page logged in as "admin"
|
||||
|
||||
# Open a new tab on the same page.
|
||||
When I open a tab named "CourseViewer1" on the current page
|
||||
And I should see "Course 1" in the "h1" "css_element"
|
||||
And I am on the "C2" "Course" page
|
||||
|
||||
# Open new tab for specified page with identifier.
|
||||
And I open a tab named "CourseViewer2" on the "C3" "Course" page
|
||||
|
||||
# And for a specified page without identifier.
|
||||
And I open a tab named "CourseViewer4" on the "My courses" page
|
||||
|
||||
# Switch between all the tabs and confirm their different contents.
|
||||
Then I should see "No courses"
|
||||
And I switch to "CourseViewer2" tab
|
||||
And "Course 3" "heading" should exist
|
||||
And I switch to "CourseViewer1" tab
|
||||
And "Course 2" "heading" should exist
|
||||
And I switch to the main tab
|
||||
And "Course 1" "heading" should exist
|
||||
+46
-1
@@ -170,6 +170,19 @@ class brickfield_accessibility_color_test extends brickfield_accessibility_test
|
||||
'yellowgreen' => '9acd32'
|
||||
];
|
||||
|
||||
/** @var string[] Define estimated relative font-size codes to pt values. */
|
||||
public $fontsizenames = [
|
||||
'xx-small' => 9,
|
||||
'x-small' => 10,
|
||||
'small' => 11,
|
||||
'smaller' => 11,
|
||||
'medium' => 12,
|
||||
'large' => 14,
|
||||
'larger' => 14,
|
||||
'x-large' => 18,
|
||||
'xx-large' => 24,
|
||||
];
|
||||
|
||||
/**
|
||||
* Helper method that finds the luminosity between the provided
|
||||
* foreground and background parameters.
|
||||
@@ -227,7 +240,8 @@ class brickfield_accessibility_color_test extends brickfield_accessibility_test
|
||||
$l2 = (.2126 * $r4 + 0.7152 * $g4 + 0.0722 * $b4);
|
||||
}
|
||||
|
||||
$luminosity = round(($l1 + 0.05) / ($l2 + 0.05), 2);
|
||||
// Increase round to 4 to avoid a 4.49 contrast being round up to a false pass of 4.5.
|
||||
$luminosity = round(($l1 + 0.05) / ($l2 + 0.05), 4);
|
||||
return $luminosity;
|
||||
}
|
||||
|
||||
@@ -362,4 +376,35 @@ class brickfield_accessibility_color_test extends brickfield_accessibility_test
|
||||
: $backrgb['b'] - $forergb['b'];
|
||||
return ['red' => $reddiff, 'green' => $greendiff, 'blue' => $bluediff];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that finds the estimated font-size for the provided
|
||||
* string font-size parameter.
|
||||
* @param string $fontsize The css font-size, in various formats
|
||||
* @return int The estimated font-size
|
||||
*/
|
||||
public function get_fontsize(string $fontsize): int {
|
||||
$newfontsize = 12; // Default value, in pt, equivalent to 16px.
|
||||
|
||||
// Search for rem, em, and px initially, typical font-size values.
|
||||
$pos1 = stripos($fontsize, 'rem');
|
||||
$pos2 = stripos($fontsize, 'em');
|
||||
$pos3 = stripos($fontsize, 'px');
|
||||
if ($pos1 !== false) {
|
||||
$rem = substr($fontsize, 0, -3);
|
||||
$newfontsize = $newfontsize * $rem;
|
||||
} else if ($pos2 !== false) {
|
||||
$em = substr($fontsize, 0, -2);
|
||||
$newfontsize = $newfontsize * $em;
|
||||
} else if ($pos3 !== false) {
|
||||
$px = substr($fontsize, 0, -2);
|
||||
$newfontsize = 0.75 * $px;
|
||||
} else if (in_array($fontsize, array_keys($this->fontsizenames))) {
|
||||
$newfontsize = $this->fontsizenames[$fontsize];
|
||||
} else {
|
||||
preg_match_all('!\d+!', $fontsize, $matches);
|
||||
$newfontsize = $matches[0][0] ?? $newfontsize;
|
||||
}
|
||||
return (int) $newfontsize;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -92,8 +92,7 @@ class css_text_has_contrast extends brickfield_accessibility_color_test {
|
||||
$italic = false;
|
||||
|
||||
if (isset($style['font-size'])) {
|
||||
preg_match_all('!\d+!', $style['font-size'], $matches);
|
||||
$fontsize = $matches[0][0];
|
||||
$fontsize = $this->get_fontsize($style['font-size']);
|
||||
}
|
||||
|
||||
if (isset($style['font-weight'])) {
|
||||
|
||||
+112
@@ -178,6 +178,54 @@ EOD;
|
||||
</html>
|
||||
EOD;
|
||||
|
||||
/** @var string HTML with px18 fail colour values. */
|
||||
private $px18 = <<<EOD
|
||||
<body><p style="color:#EF0000; background-color:white; font-size: 18px">
|
||||
This is not contrasty enough.</p></body>
|
||||
EOD;
|
||||
|
||||
/** @var string HTML with px19bold pass colour values. */
|
||||
private $px19bold = <<<EOD
|
||||
<body><p style="color:#EF0000; background-color:white; font-size: 19px; font-weight: bold;">
|
||||
This is contrasty enough.</p></body>
|
||||
EOD;
|
||||
|
||||
/** @var string HTML with px18 pass colour values. */
|
||||
private $px18pass = <<<EOD
|
||||
<body><p style="color:#E60000; background-color:white; font-size: 18px">
|
||||
This is contrasty enough.</p></body>
|
||||
EOD;
|
||||
|
||||
/** @var string HTML with medium size colour values. */
|
||||
private $mediumfail = <<<EOD
|
||||
<body><p style="color:#EF0000; background-color:white; font-size: medium">
|
||||
This is not contrasty enough.</p></body>
|
||||
EOD;
|
||||
|
||||
/** @var string HTML with px18 colour values. */
|
||||
private $mediumpass = <<<EOD
|
||||
<body><p style="color:#E60000; background-color:white; font-size: medium">
|
||||
This is contrasty enough.</p></body>
|
||||
EOD;
|
||||
|
||||
/** @var string HTML with larger fail colour values. */
|
||||
private $largerfail = <<<EOD
|
||||
<body><p style="color:#FF6161; background-color:white; font-size: larger">
|
||||
This is not contrasty enough.</p></body>
|
||||
EOD;
|
||||
|
||||
/** @var string HTML with px18 colour values. */
|
||||
private $largerpass = <<<EOD
|
||||
<body><p style="color:#FF5C5C; background-color:white; font-size: larger;">
|
||||
This is contrasty enough.</p></body>
|
||||
EOD;
|
||||
|
||||
/** @var string HTML with px18 colour values. */
|
||||
private $largerboldpass = <<<EOD
|
||||
<body><p style="color:#FF5C5C; background-color:white; font-size: larger; font-weight: bold;">
|
||||
This is contrasty enough.</p></body>
|
||||
EOD;
|
||||
|
||||
/**
|
||||
* Test for the area assign intro
|
||||
*/
|
||||
@@ -235,4 +283,68 @@ EOD;
|
||||
$results = $this->get_checker_results($this->emptyvalue);
|
||||
$this->assertEmpty($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for text px18 with insufficient contrast of 4.49.
|
||||
*/
|
||||
public function test_check_for_px18_fail() {
|
||||
$results = $this->get_checker_results($this->px18);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for text px19 bold with sufficient contrast of 4.49.
|
||||
*/
|
||||
public function test_check_for_px19bold_pass() {
|
||||
$results = $this->get_checker_results($this->px19bold);
|
||||
$this->assertEmpty($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for text px18 with sufficient contrast of 4.81.
|
||||
*/
|
||||
public function test_check_for_px18_pass() {
|
||||
$results = $this->get_checker_results($this->px18pass);
|
||||
$this->assertEmpty($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for medium (12pt) text with insufficient contrast of 4.49.
|
||||
*/
|
||||
public function test_check_for_medium_fail() {
|
||||
$results = $this->get_checker_results($this->mediumfail);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for medium (12pt) text with sufficient contrast of 4.81.
|
||||
*/
|
||||
public function test_check_for_medium_pass() {
|
||||
$results = $this->get_checker_results($this->mediumpass);
|
||||
$this->assertEmpty($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for larger (14pt) text with insufficient contrast of 2.94.
|
||||
*/
|
||||
public function test_check_for_larger_fail() {
|
||||
$results = $this->get_checker_results($this->largerfail);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for larger (14pt) text with insufficient contrast of 3.02.
|
||||
*/
|
||||
public function test_check_for_larger_pass() {
|
||||
$results = $this->get_checker_results($this->largerpass);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for larger (14pt) bold text with sufficient contrast of 3.02.
|
||||
*/
|
||||
public function test_check_for_largerbold_pass() {
|
||||
$results = $this->get_checker_results($this->largerboldpass);
|
||||
$this->assertEmpty($results);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ to the element that will trigger the confirmation modal.
|
||||
|
||||
## Source files
|
||||
|
||||
* `lib/amd/src/confirm.js` ({{< jsdoc module="core/confirm" >}})
|
||||
* `lib/amd/src/utility.js` ({{< jsdoc module="core/utility" >}})
|
||||
* `lib/templates/modal.mustache`
|
||||
|
||||
## Usage
|
||||
@@ -24,7 +24,7 @@ The confirmation AMD module is loaded automatically, so the only thing you need
|
||||
to the target element:
|
||||
{{< highlight html >}}
|
||||
<button type="button" class="btn btn-primary" data-confirmation="modal" data-confirmation-title-str='["delete", "core"]'
|
||||
data-confirmation-question-str='["areyousure"]' data-confirmation-yes-button-str='["delete", "core"]'>Show confirmation modal</button>
|
||||
data-confirmation-content-str='["areyousure"]' data-confirmation-yes-button-str='["delete", "core"]'>Show confirmation modal</button>
|
||||
{{< /highlight >}}
|
||||
|
||||
You can also use it on PHP, you just need to set the attributes parameter to any moodle output component that takes attributes:
|
||||
@@ -32,7 +32,7 @@ You can also use it on PHP, you just need to set the attributes parameter to any
|
||||
echo $OUTPUT->single_button('#', get_string('delete'), 'get', [
|
||||
'data-confirmation' => 'modal',
|
||||
'data-confirmation-title-str' => json_encode(['delete', 'core']),
|
||||
'data-confirmation-question-str' => json_encode(['areyousure']),
|
||||
'data-confirmation-content-str' => json_encode(['areyousure']),
|
||||
'data-confirmation-yes-button-str' => json_encode(['delete', 'core'])
|
||||
]);
|
||||
{{< / php >}}
|
||||
@@ -56,8 +56,8 @@ echo $OUTPUT->single_button('#', get_string('delete'), 'get', [
|
||||
<td>The modal title language string identifier, must be provided in JSON encoded format.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>data-confirmation-question-str</td>
|
||||
<td>The confirmation question language string identifier, must be provided in JSON encoded format.</td>
|
||||
<td>data-confirmation-content-str</td>
|
||||
<td>The confirmation modal main content language string identifier, must be provided in JSON encoded format.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>data-confirmation-yes-button-str</td>
|
||||
@@ -84,14 +84,14 @@ echo $OUTPUT->single_button('#', get_string('delete'), 'get', [
|
||||
|
||||
{{< example >}}
|
||||
<button type="button" class="btn btn-primary" data-confirmation="modal" data-confirmation-title-str='["delete", "core"]'
|
||||
data-confirmation-question-str='["areyousure"]' data-confirmation-yes-button-str='["delete", "core"]'>Show confirmation modal</button>
|
||||
data-confirmation-content-str='["areyousure"]' data-confirmation-yes-button-str='["delete", "core"]'>Show confirmation modal</button>
|
||||
{{< /example >}}
|
||||
|
||||
### Confirmation modal with a toast
|
||||
|
||||
{{< example >}}
|
||||
<button type="button" class="btn btn-primary" data-confirmation="modal" data-confirmation-title-str='["delete", "core"]'
|
||||
data-confirmation-question-str='["areyousure"]' data-confirmation-yes-button-str='["delete", "core"]' data-confirmation-toast="true"
|
||||
data-confirmation-content-str='["areyousure"]' data-confirmation-yes-button-str='["delete", "core"]' data-confirmation-toast="true"
|
||||
data-confirmation-toast-confirmation-str='["deleteblockinprogress", "block", "Online users"]'>Show confirmation modal</button>
|
||||
{{< /example >}}
|
||||
|
||||
@@ -99,6 +99,6 @@ data-confirmation-toast-confirmation-str='["deleteblockinprogress", "block", "On
|
||||
|
||||
{{< example >}}
|
||||
<button type="button" class="btn btn-primary" data-confirmation="modal" data-confirmation-title-str='["delete", "core"]'
|
||||
data-confirmation-question-str='["areyousure"]' data-confirmation-yes-button-str='["delete", "core"]'
|
||||
data-confirmation-content-str='["areyousure"]' data-confirmation-yes-button-str='["delete", "core"]'
|
||||
data-confirmation-destination="http://moodle.com">Show confirmation modal</button>
|
||||
{{< /example >}}
|
||||
|
||||
@@ -120,8 +120,7 @@ Feature: Add customised file types
|
||||
| Custom description | Froggy file |
|
||||
And I press "Save changes"
|
||||
# Create a resource activity and add it to a course
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
When I add a "File" to section "1"
|
||||
When I add a resource activity to course "Course 1" section "1"
|
||||
And I set the following fields to these values:
|
||||
| Name | An example of customised file type |
|
||||
| Description | File description |
|
||||
|
||||
@@ -205,7 +205,7 @@ class tool_generator_course_backend extends tool_generator_backend {
|
||||
* @return int Course id
|
||||
*/
|
||||
public function make() {
|
||||
global $DB, $CFG;
|
||||
global $DB, $CFG, $USER;
|
||||
require_once($CFG->dirroot . '/lib/phpunit/classes/util.php');
|
||||
|
||||
raise_memory_limit(MEMORY_EXTRA);
|
||||
@@ -231,6 +231,12 @@ class tool_generator_course_backend extends tool_generator_backend {
|
||||
$this->create_users();
|
||||
$this->create_forum();
|
||||
|
||||
// We are checking 'enroladminnewcourse' setting to decide to enrol admins or not.
|
||||
if (!empty($CFG->creatornewroleid) && !empty($CFG->enroladminnewcourse) && is_siteadmin($USER->id)) {
|
||||
// Deal with course creators - enrol them internally with default role.
|
||||
enrol_try_internal_enrol($this->course->id, $USER->id, $CFG->creatornewroleid);
|
||||
}
|
||||
|
||||
// Log total time.
|
||||
$this->log('coursecompleted', round(microtime(true) - $entirestart, 1));
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
@core @core_course @tool_generator
|
||||
Feature: Admins can create test courses
|
||||
In order to create testing information
|
||||
As an admin
|
||||
I need to create testing courses quickly
|
||||
|
||||
@javascript
|
||||
Scenario: 'Auto-enrol admin in new courses' setting when creating a test course as admin
|
||||
Given I log in as "admin"
|
||||
And the following config values are set as admin:
|
||||
| enroladminnewcourse | 0 |
|
||||
And I navigate to "Development > Make test course" in site administration
|
||||
And I set the following fields to these values:
|
||||
| Size of course | XS |
|
||||
| Course full name | Fake course for testing |
|
||||
| Course short name | fake |
|
||||
And I press "Create course"
|
||||
And I click on "Continue" "link"
|
||||
And I navigate to course participants
|
||||
Then I should not see "Teacher"
|
||||
And I should not see "Nothing to display"
|
||||
And the following config values are set as admin:
|
||||
| enroladminnewcourse | 1 |
|
||||
And I navigate to "Courses > Add a new course" in site administration
|
||||
And I navigate to "Development > Make test course" in site administration
|
||||
And I set the following fields to these values:
|
||||
| Size of course | XS |
|
||||
| Course full name | New fake course for testing |
|
||||
| Course short name | newfake |
|
||||
And I press "Create course"
|
||||
And I click on "Continue" "link"
|
||||
And I navigate to course participants
|
||||
And I should see "Teacher"
|
||||
@@ -68,9 +68,14 @@ class maketestcourse_test extends \advanced_testcase {
|
||||
$this->assertEquals(2, count($modinfo->get_section_info_all()));
|
||||
|
||||
// Check user is enrolled.
|
||||
// enroladminnewcourse is enabled by default, so admin is also enrolled as teacher.
|
||||
$users = get_enrolled_users($context);
|
||||
$this->assertEquals(1, count($users));
|
||||
$this->assertEquals('tool_generator_000001', reset($users)->username);
|
||||
$this->assertEquals(2, count($users));
|
||||
$usernames = array_map(function($user) {
|
||||
return $user->username;
|
||||
}, $users);
|
||||
$this->assertTrue(in_array('admin', $usernames));
|
||||
$this->assertTrue(in_array('tool_generator_000001', $usernames));
|
||||
|
||||
// Check there's a page on the course.
|
||||
$pages = $modinfo->get_instances_of('page');
|
||||
|
||||
@@ -114,7 +114,7 @@ if ($mode == DELETION_OF_SELECTED_LANG and (!empty($uninstalllang) or !empty($co
|
||||
}
|
||||
}
|
||||
|
||||
if ($mode == UPDATE_ALL_LANG) {
|
||||
if ($mode == UPDATE_ALL_LANG && confirm_sesskey()) {
|
||||
core_php_time_limit::raise();
|
||||
$controller->update_all_installed_languages();
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
<div>
|
||||
<form id="updateform" action="{{updateurl}}" method="post">
|
||||
<fieldset>
|
||||
<input type="hidden" name="sesskey" value="{{sesskey}}">
|
||||
<input type="submit" value="{{#str}}updatelangs, tool_langimport{{/str}}" class="btn btn-secondary">
|
||||
{{#hasmanyinstalledlanguages}}
|
||||
<p class="mt-2 small">{{#str}} updatelangsnote, tool_langimport, {"taskname": {{#quote}}{{updatelangstaskname}}{{/quote}} }{{/str}}</p>
|
||||
|
||||
@@ -75,7 +75,7 @@ trait reader {
|
||||
*/
|
||||
public static function decode_other(?string $other) {
|
||||
if ($other === 'N;' || preg_match('~^.:~', $other ?? '')) {
|
||||
return unserialize($other);
|
||||
return unserialize($other, ['allowed_classes' => [stdClass::class]]);
|
||||
} else {
|
||||
return json_decode($other ?? '', true);
|
||||
}
|
||||
|
||||
@@ -50,12 +50,10 @@ list($options, $unrecognized) = cli_get_params(
|
||||
)
|
||||
);
|
||||
|
||||
if (file_exists(__DIR__.'/../../../../vendor/phpunit/phpunit/composer.json')) {
|
||||
// Composer packages present.
|
||||
require_once(__DIR__.'/../../../../vendor/autoload.php');
|
||||
|
||||
} else {
|
||||
// Note: installation via PEAR is not supported any more.
|
||||
// Basic check to see if phpunit is installed.
|
||||
if (!file_exists(__DIR__.'/../../../../vendor/phpunit/phpunit/composer.json') ||
|
||||
!file_exists(__DIR__.'/../../../../vendor/bin/phpunit') ||
|
||||
!file_exists(__DIR__.'/../../../../vendor/autoload.php')) {
|
||||
phpunit_bootstrap_error(PHPUNIT_EXITCODE_PHPUNITMISSING);
|
||||
}
|
||||
|
||||
@@ -74,12 +72,13 @@ if ($options['run']) {
|
||||
}
|
||||
}
|
||||
$_SERVER['argv'] = array_values($_SERVER['argv']);
|
||||
PHPUnit\TextUI\Command::main();
|
||||
require(__DIR__ . '/../../../../vendor/bin/phpunit');
|
||||
exit(0);
|
||||
}
|
||||
|
||||
define('PHPUNIT_UTIL', true);
|
||||
|
||||
require(__DIR__.'/../../../../vendor/autoload.php');
|
||||
require(__DIR__ . '/../../../../lib/phpunit/bootstrap.php');
|
||||
|
||||
// from now on this is a regular moodle CLI_SCRIPT
|
||||
|
||||
@@ -37,3 +37,13 @@ function tool_task_status_checks() : array {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Function used to handle mtrace by outputting the text to normal browser window.
|
||||
*
|
||||
* @param string $message Message to output
|
||||
* @param string $eol End of line character
|
||||
*/
|
||||
function tool_task_mtrace_wrapper(string $message, string $eol): void {
|
||||
$message = s($message);
|
||||
echo $message . $eol;
|
||||
}
|
||||
|
||||
@@ -30,16 +30,6 @@ require('../../../config.php');
|
||||
|
||||
require_once($CFG->libdir.'/cronlib.php');
|
||||
|
||||
/**
|
||||
* Function used to handle mtrace by outputting the text to normal browser window.
|
||||
*
|
||||
* @param string $message Message to output
|
||||
* @param string $eol End of line character
|
||||
*/
|
||||
function tool_task_mtrace_wrapper($message, $eol) {
|
||||
echo s($message . $eol);
|
||||
}
|
||||
|
||||
// Allow execution of single task. This requires login and has different rules.
|
||||
$taskname = required_param('task', PARAM_RAW_TRIMMED);
|
||||
|
||||
@@ -97,6 +87,8 @@ require_sesskey();
|
||||
|
||||
// Prepare to handle output via mtrace.
|
||||
echo html_writer::start_tag('pre');
|
||||
|
||||
require_once("{$CFG->dirroot}/{$CFG->admin}/tool/task/lib.php");
|
||||
$CFG->mtrace_wrapper = 'tool_task_mtrace_wrapper';
|
||||
|
||||
// Run the specified task (this will output an error if it doesn't exist).
|
||||
|
||||
@@ -236,6 +236,8 @@ class form_test extends \advanced_testcase {
|
||||
$checker->set_day_of_week('6');
|
||||
$this->assertTrue($checker->is_valid($checker::FIELD_DAYOFWEEK));
|
||||
$checker->set_day_of_week('7');
|
||||
$this->assertTrue($checker->is_valid($checker::FIELD_DAYOFWEEK));
|
||||
$checker->set_day_of_week('8');
|
||||
$this->assertFalse($checker->is_valid($checker::FIELD_DAYOFWEEK));
|
||||
$checker->set_day_of_week('20');
|
||||
$this->assertFalse($checker->is_valid($checker::FIELD_DAYOFWEEK));
|
||||
@@ -247,7 +249,7 @@ class form_test extends \advanced_testcase {
|
||||
$checker->set_day_of_week('*/6');
|
||||
$this->assertTrue($checker->is_valid($checker::FIELD_DAYOFWEEK));
|
||||
$checker->set_day_of_week('*/7');
|
||||
$this->assertFalse($checker->is_valid($checker::FIELD_DAYOFWEEK));
|
||||
$this->assertTrue($checker->is_valid($checker::FIELD_DAYOFWEEK));
|
||||
$checker->set_day_of_week('*/13');
|
||||
$this->assertFalse($checker->is_valid($checker::FIELD_DAYOFWEEK));
|
||||
$checker->set_day_of_week('*/35');
|
||||
@@ -271,6 +273,8 @@ class form_test extends \advanced_testcase {
|
||||
$checker->set_day_of_week('65-2');
|
||||
$this->assertFalse($checker->is_valid($checker::FIELD_DAYOFWEEK));
|
||||
$checker->set_day_of_week('3-7');
|
||||
$this->assertTrue($checker->is_valid($checker::FIELD_DAYOFWEEK));
|
||||
$checker->set_day_of_week('3-8');
|
||||
$this->assertFalse($checker->is_valid($checker::FIELD_DAYOFWEEK));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ class login implements renderable, templatable {
|
||||
$languagedata = new \core\output\language_menu($PAGE);
|
||||
|
||||
$this->languagemenu = $languagedata->export_for_action_menu($OUTPUT);
|
||||
$this->canloginasguest = $CFG->guestloginbutton and !isguestuser();
|
||||
$this->canloginasguest = $CFG->guestloginbutton && !isguestuser();
|
||||
$this->canloginbyemail = !empty($CFG->authloginviaemail);
|
||||
$this->cansignup = $CFG->registerauth == 'email' || !empty($CFG->registerauth);
|
||||
if ($CFG->rememberusername == 0) {
|
||||
|
||||
@@ -149,15 +149,23 @@ class condition extends \core_availability\condition {
|
||||
|
||||
$allow = true;
|
||||
if ($this->expectedcompletion == COMPLETION_COMPLETE) {
|
||||
// Complete also allows the pass, fail states.
|
||||
// Complete also allows the pass state.
|
||||
switch ($completiondata->completionstate) {
|
||||
case COMPLETION_COMPLETE:
|
||||
case COMPLETION_COMPLETE_FAIL:
|
||||
case COMPLETION_COMPLETE_PASS:
|
||||
break;
|
||||
default:
|
||||
$allow = false;
|
||||
}
|
||||
} else if ($this->expectedcompletion == COMPLETION_INCOMPLETE) {
|
||||
// Incomplete also allows the fail state.
|
||||
switch ($completiondata->completionstate) {
|
||||
case COMPLETION_INCOMPLETE:
|
||||
case COMPLETION_COMPLETE_FAIL:
|
||||
break;
|
||||
default:
|
||||
$allow = false;
|
||||
}
|
||||
} else {
|
||||
// Other values require exact match.
|
||||
if ($completiondata->completionstate != $this->expectedcompletion) {
|
||||
|
||||
@@ -74,3 +74,48 @@ Feature: availability_completion
|
||||
And I click on "forum 1" "link" in the "region-main" "region"
|
||||
And I am on "Course 1" course homepage
|
||||
And I should see "Page 2" in the "region-main" "region"
|
||||
|
||||
@javascript
|
||||
Scenario Outline: Restrict access for activity completion should display correctly
|
||||
Given the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | questiontext |
|
||||
| Test questions | truefalse | First question | Answer the first question |
|
||||
And the following "activities" exist:
|
||||
| activity | name | course | idnumber | gradepass | completion | completionpassgrade | completionusegrade |
|
||||
| quiz | Test quiz name | C1 | quiz1 | 5.00 | 2 | 1 | 1 |
|
||||
And quiz "Test quiz name" contains the following questions:
|
||||
| question | page |
|
||||
| First question | 1 |
|
||||
And I am on the "Page 2" "page activity editing" page logged in as "teacher1"
|
||||
And I expand all fieldsets
|
||||
And I press "Add restriction..."
|
||||
And I click on "Activity completion" "button" in the "Add restriction..." "dialogue"
|
||||
And I click on ".availability-item .availability-eye img" "css_element"
|
||||
And I set the following fields to these values:
|
||||
| Required completion status | <condition> |
|
||||
| cm | quiz |
|
||||
And I press "Save and return to course"
|
||||
And I am on the "Course 1" "course" page logged in as "student1"
|
||||
And I <shouldornot> see "Page 2" in the "region-main" "region"
|
||||
# Failed grade for quiz.
|
||||
When user "student1" has attempted "Test quiz name" with responses:
|
||||
| slot | response |
|
||||
| 1 | <answer1> |
|
||||
And I reload the page
|
||||
And I <shouldornotanswer1> see "Page 2" in the "region-main" "region"
|
||||
# Passing grade for quiz.
|
||||
But user "student1" has attempted "Test quiz name" with responses:
|
||||
| slot | response |
|
||||
| 1 | <answer2> |
|
||||
And I reload the page
|
||||
And I <shouldornotanswer2> see "Page 2" in the "region-main" "region"
|
||||
|
||||
Examples:
|
||||
| condition | answer1 | answer2 | shouldornot | shouldornotanswer1 | shouldornotanswer2 |
|
||||
| must be marked complete | False | True | should not | should not | should |
|
||||
| must not be marked complete | False | True | should | should | should not |
|
||||
| must be complete with pass grade | False | True | should not | should not | should |
|
||||
| must be complete with fail grade | False | True | should not | should | should not |
|
||||
|
||||
@@ -301,14 +301,14 @@ class condition_test extends \advanced_testcase {
|
||||
$cond = new condition((object)[
|
||||
'cm' => (int)$assigncm->id, 'e' => COMPLETION_INCOMPLETE
|
||||
]);
|
||||
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
|
||||
$this->assertTrue($cond->is_available(true, $info, true, $user->id));
|
||||
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
|
||||
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
|
||||
|
||||
$cond = new condition((object)[
|
||||
'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE
|
||||
]);
|
||||
$this->assertTrue($cond->is_available(false, $info, true, $user->id));
|
||||
$this->assertFalse($cond->is_available(true, $info, true, $user->id));
|
||||
$this->assertFalse($cond->is_available(false, $info, true, $user->id));
|
||||
$this->assertTrue($cond->is_available(true, $info, true, $user->id));
|
||||
|
||||
$cond = new condition((object)[
|
||||
'cm' => (int)$assigncm->id, 'e' => COMPLETION_COMPLETE_PASS
|
||||
@@ -531,10 +531,10 @@ class condition_test extends \advanced_testcase {
|
||||
],
|
||||
// Depending on assign with grade.
|
||||
'Previous complete condition with previous fail grade' => [
|
||||
40, COMPLETION_COMPLETE, '', 'page3', true, false, '~Assign!.*is marked complete~'
|
||||
40, COMPLETION_COMPLETE, '', 'page3', false, true, '~Assign!.*is marked complete~',
|
||||
],
|
||||
'Previous incomplete condition with previous fail grade' => [
|
||||
40, COMPLETION_INCOMPLETE, '', 'page3', false, true, '~Assign!.*is incomplete~'
|
||||
40, COMPLETION_INCOMPLETE, '', 'page3', true, false, '~Assign!.*is incomplete~',
|
||||
],
|
||||
'Previous complete pass condition with previous fail grade' => [
|
||||
40, COMPLETION_COMPLETE_PASS, '', 'page3', false, true, '~Assign!.*is complete and passed~'
|
||||
|
||||
@@ -1602,7 +1602,13 @@ class backup_block_instance_structure_step extends backup_structure_step {
|
||||
// Transform configdata information if needed (process links and friends)
|
||||
$blockrec = $DB->get_record('block_instances', array('id' => $this->task->get_blockid()));
|
||||
if ($attrstotransform = $this->task->get_configdata_encoded_attributes()) {
|
||||
$configdata = (array)unserialize(base64_decode($blockrec->configdata));
|
||||
$configdata = array_filter(
|
||||
(array) unserialize_object(base64_decode($blockrec->configdata)),
|
||||
static function($value): bool {
|
||||
return !($value instanceof __PHP_Incomplete_Class);
|
||||
}
|
||||
);
|
||||
|
||||
foreach ($configdata as $attribute => $value) {
|
||||
if (in_array($attribute, $attrstotransform)) {
|
||||
$configdata[$attribute] = $this->contenttransformer->process($value);
|
||||
|
||||
@@ -4340,7 +4340,13 @@ class restore_block_instance_structure_step extends restore_structure_step {
|
||||
// Let's look for anything within configdata neededing processing
|
||||
// (nulls and uses of legacy file.php)
|
||||
if ($attrstotransform = $this->task->get_configdata_encoded_attributes()) {
|
||||
$configdata = (array) unserialize_object(base64_decode($data->configdata));
|
||||
$configdata = array_filter(
|
||||
(array) unserialize_object(base64_decode($data->configdata)),
|
||||
static function($value): bool {
|
||||
return !($value instanceof __PHP_Incomplete_Class);
|
||||
}
|
||||
);
|
||||
|
||||
foreach ($configdata as $attribute => $value) {
|
||||
if (in_array($attribute, $attrstotransform)) {
|
||||
$configdata[$attribute] = $this->contentprocessor->process_cdata($value);
|
||||
|
||||
@@ -160,12 +160,16 @@ if ($context->contextlevel == CONTEXT_MODULE) {
|
||||
$renderer = $PAGE->get_renderer('core', 'backup');
|
||||
echo $renderer->backup_files_viewer($treeview_options);
|
||||
echo $OUTPUT->container_end();
|
||||
// Update the course context with the proper value, because $context contains the module context.
|
||||
$coursecontext = \context_course::instance($course->id);
|
||||
} else {
|
||||
$coursecontext = $context;
|
||||
}
|
||||
|
||||
echo $OUTPUT->heading_with_help(get_string('choosefilefromcoursebackup', 'backup'), 'choosefilefromcoursebackup', 'backup');
|
||||
echo $OUTPUT->container_start();
|
||||
$treeview_options = array();
|
||||
$treeview_options['filecontext'] = $context;
|
||||
$treeview_options['filecontext'] = $coursecontext;
|
||||
$treeview_options['currentcontext'] = $context;
|
||||
$treeview_options['component'] = 'backup';
|
||||
$treeview_options['context'] = $context;
|
||||
|
||||
@@ -14,20 +14,17 @@
|
||||
// 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();
|
||||
|
||||
use \core_badges\badge;
|
||||
|
||||
/**
|
||||
* Local stuff for category enrolment plugin.
|
||||
* Event observer for badges.
|
||||
*
|
||||
* @package core_badges
|
||||
* @copyright 2013 Rajesh Taneja <rajesh@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use \core_badges\badge;
|
||||
/**
|
||||
* Event observer for badges.
|
||||
*/
|
||||
class core_badges_observer {
|
||||
/**
|
||||
* Triggered when 'course_module_completion_updated' event is triggered.
|
||||
|
||||
@@ -188,8 +188,6 @@ class award_criteria_activity extends award_criteria {
|
||||
* @return bool Whether criteria is complete
|
||||
*/
|
||||
public function review($userid, $filtered = false) {
|
||||
$completionstates = array(COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS, COMPLETION_COMPLETE_FAIL);
|
||||
|
||||
if ($this->course->startdate > time()) {
|
||||
return false;
|
||||
}
|
||||
@@ -209,6 +207,15 @@ class award_criteria_activity extends award_criteria {
|
||||
$check_date = ($date <= $param['bydate']);
|
||||
}
|
||||
|
||||
// Successfull completion states depend on the completion settings.
|
||||
if (isset($data->passgrade)) {
|
||||
// Passing grade is required. Don't issue a badge when state is COMPLETION_COMPLETE_FAIL.
|
||||
$completionstates = [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS];
|
||||
} else {
|
||||
// Any grade is required. Issue a badge even when state is COMPLETION_COMPLETE_FAIL.
|
||||
$completionstates = [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS, COMPLETION_COMPLETE_FAIL];
|
||||
}
|
||||
|
||||
if ($this->method == BADGE_CRITERIA_AGGREGATION_ALL) {
|
||||
if (in_array($data->completionstate, $completionstates) && $check_date) {
|
||||
$overall = true;
|
||||
|
||||
@@ -54,6 +54,8 @@ $badge = new badge($badgeid);
|
||||
$context = $badge->get_context();
|
||||
$navurl = new moodle_url('/badges/index.php', array('type' => $badge->type));
|
||||
|
||||
require_capability('moodle/badges:viewawarded', $context);
|
||||
|
||||
if ($badge->type == BADGE_TYPE_COURSE) {
|
||||
if (empty($CFG->badges_allowcoursebadges)) {
|
||||
throw new \moodle_exception('coursebadgesdisabled', 'badges');
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
@mod @mod_quiz @core @core_badges @javascript
|
||||
@mod @mod_quiz @core @core_badges @core_completion @javascript
|
||||
Feature: Award badges based on activity completion
|
||||
In order to ensure a student has learned the material before being marked complete
|
||||
As a teacher
|
||||
I need to set a quiz to award a badge when upon completion when the student receives a passing grade, or completed_fail if they use all attempts without passing
|
||||
I need to configure an activity to grant a badge only when the student achieves a passing grade upon completion.
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| student1 | Student | 1 | student1@example.com |
|
||||
| student2 | Student | 1 | student2@example.com |
|
||||
| student2 | Student | 2 | student2@example.com |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category | enablecompletion |
|
||||
@@ -27,96 +27,103 @@ Feature: Award badges based on activity completion
|
||||
| questioncategory | qtype | name | questiontext |
|
||||
| Test questions | truefalse | First question | Answer the first question |
|
||||
And the following "activities" exist:
|
||||
| activity | name | course | idnumber | attempts | gradepass | completion | completionattemptsexhausted | completionpassgrade | completionusegrade |
|
||||
| quiz | Test quiz name | C1 | quiz1 | 2 | 5.00 | 2 | 1 | 1 | 1 |
|
||||
And quiz "Test quiz name" contains the following questions:
|
||||
| activity | name | course | idnumber | attempts | gradepass | completion | completionpassgrade | completionusegrade |
|
||||
| quiz | Test quiz name 1 | C1 | quiz1 | 2 | 5.00 | 2 | 1 | 1 |
|
||||
| quiz | Test quiz name 2 | C1 | quiz2 | 2 | 5.00 | 2 | 0 | 1 |
|
||||
And quiz "Test quiz name 1" contains the following questions:
|
||||
| question | page |
|
||||
| First question | 1 |
|
||||
And quiz "Test quiz name 2" contains the following questions:
|
||||
| question | page |
|
||||
| First question | 1 |
|
||||
And user "student1" has attempted "Test quiz name" with responses:
|
||||
| slot | response |
|
||||
| 1 | False |
|
||||
And user "student2" has attempted "Test quiz name" with responses:
|
||||
| slot | response |
|
||||
| 1 | False |
|
||||
And the following "core_badges > Badge" exists:
|
||||
| name | Course Badge |
|
||||
| name | Course Badge 1 |
|
||||
| status | 0 |
|
||||
| type | 2 |
|
||||
| course | C1 |
|
||||
| description | Course badge description |
|
||||
| description | Course badge 1 description |
|
||||
| image | badges/tests/behat/badge.png |
|
||||
And the following "core_badges > Badge" exists:
|
||||
| name | Course Badge 2 |
|
||||
| status | 0 |
|
||||
| type | 2 |
|
||||
| course | C1 |
|
||||
| description | Course badge 2 description |
|
||||
| image | badges/tests/behat/badge.png |
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
|
||||
Scenario: Student earns a badge using activity completion, but does not get passing grade
|
||||
Given I am on the "Course 1" course page logged in as teacher1
|
||||
And I navigate to "Badges" in current page administration
|
||||
Scenario: Student does not earn a badge using activity completion when does not get passing grade
|
||||
Given I navigate to "Badges" in current page administration
|
||||
And I press "Manage badges"
|
||||
And I follow "Course Badge"
|
||||
And I follow "Course Badge 1"
|
||||
And I select "Criteria" from the "jump" singleselect
|
||||
And I set the field "type" to "Activity completion"
|
||||
And I set the field "Quiz - Test quiz name" to "1"
|
||||
And I set the field "Quiz - Test quiz name 1" to "1"
|
||||
And I press "Save"
|
||||
And I press "Enable access"
|
||||
And I press "Continue"
|
||||
And I should see "Recipients (0)"
|
||||
And I log out
|
||||
And I am on the "Course 1" course page logged in as student1
|
||||
And the "Receive a grade" completion condition of "Test quiz name" is displayed as "done"
|
||||
And the "Receive a passing grade" completion condition of "Test quiz name" is displayed as "failed"
|
||||
And the "Receive a pass grade or complete all available attempts" completion condition of "Test quiz name" is displayed as "todo"
|
||||
When I am on the "Test quiz name" "quiz activity" page
|
||||
And I press "Re-attempt quiz"
|
||||
And I set the field "False" to "1"
|
||||
And I press "Finish attempt ..."
|
||||
And I press "Submit all and finish"
|
||||
And I click on "Submit" "button" in the "Submit all your answers and finish?" "dialogue"
|
||||
And I log out
|
||||
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
# Pass grade for student1. Activity is considered complete because student1 got a passing grade.
|
||||
And user "student1" has attempted "Test quiz name 1" with responses:
|
||||
| slot | response |
|
||||
| 1 | True |
|
||||
# Fail grade for student2. Activity is considered incomplete because student2 got a failing grade.
|
||||
And user "student2" has attempted "Test quiz name 1" with responses:
|
||||
| slot | response |
|
||||
| 1 | False |
|
||||
And I navigate to "Badges > Manage badges" in current page administration
|
||||
And I follow "Course Badge"
|
||||
And I follow "Course Badge 1"
|
||||
Then I should see "Recipients (1)"
|
||||
And I select "Recipients (1)" from the "jump" singleselect
|
||||
And I should see "Student 1"
|
||||
And I should not see "Student 2"
|
||||
|
||||
Scenario Outline: Previously graded pass/fail students should earn a badge after enabling a badge
|
||||
Given I am on the "Course 1" course page logged in as teacher1
|
||||
And I navigate to "Badges" in current page administration
|
||||
Scenario: Students with any grades in an activity will receive a badge if the completion condition is set to receive any grade
|
||||
Given I navigate to "Badges" in current page administration
|
||||
And I press "Manage badges"
|
||||
And I follow "Course Badge"
|
||||
And I follow "Course Badge 2"
|
||||
And I select "Criteria" from the "jump" singleselect
|
||||
And I set the field "type" to "Activity completion"
|
||||
And I click on "Expand all" "link"
|
||||
And I set the field "Quiz - Test quiz name" to "1"
|
||||
And I set the field "<aggregationcriteria>" to "1"
|
||||
And I set the field "Quiz - Test quiz name 2" to "1"
|
||||
And I press "Save"
|
||||
|
||||
# Fail grade with student2
|
||||
And I am on the "Course 1" course page logged in as student2
|
||||
And I am on the "Test quiz name" "quiz activity" page
|
||||
And I press "Re-attempt quiz"
|
||||
And I set the field "False" to "1"
|
||||
And I press "Finish attempt ..."
|
||||
And I press "Submit all and finish"
|
||||
And I click on "Submit" "button" in the "Submit all your answers and finish?" "dialogue"
|
||||
And I log out
|
||||
|
||||
# Pass grade with student1
|
||||
And I am on the "Course 1" course page logged in as student1
|
||||
And I am on the "Test quiz name" "quiz activity" page
|
||||
And I press "Re-attempt quiz"
|
||||
And I set the field "False" to "0"
|
||||
And I press "Finish attempt ..."
|
||||
And I press "Submit all and finish"
|
||||
And I click on "Submit" "button" in the "Submit all your answers and finish?" "dialogue"
|
||||
And I log out
|
||||
|
||||
# Enable badge access once all students have completed an activity.
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
And I press "Enable access"
|
||||
And I press "Continue"
|
||||
# Pass grade for student1.
|
||||
And user "student1" has attempted "Test quiz name 2" with responses:
|
||||
| slot | response |
|
||||
| 1 | True |
|
||||
# Fail grade for student2. Activity is considered complete even if student2 got a failing grade.
|
||||
And user "student2" has attempted "Test quiz name 2" with responses:
|
||||
| slot | response |
|
||||
| 1 | False |
|
||||
And I navigate to "Badges > Manage badges" in current page administration
|
||||
And I follow "Course Badge"
|
||||
And I follow "Course Badge 2"
|
||||
Then I should see "Recipients (2)"
|
||||
And I select "Recipients (2)" from the "jump" singleselect
|
||||
And I should see "Student 1"
|
||||
And I should see "Student 2"
|
||||
|
||||
Scenario: Previously graded pass/fail students should earn a badge after enabling a badge
|
||||
# Pass grade for student1.
|
||||
Given user "student1" has attempted "Test quiz name 1" with responses:
|
||||
| slot | response |
|
||||
| 1 | True |
|
||||
# Fail grade for student2.
|
||||
And user "student2" has attempted "Test quiz name 1" with responses:
|
||||
| slot | response |
|
||||
| 1 | False |
|
||||
And I navigate to "Badges" in current page administration
|
||||
And I press "Manage badges"
|
||||
And I follow "Course Badge 1"
|
||||
And I select "Criteria" from the "jump" singleselect
|
||||
And I set the field "type" to "Activity completion"
|
||||
And I set the field "Quiz - Test quiz name 1" to "1"
|
||||
And I press "Save"
|
||||
# Enable badge access once students have completed the activity.
|
||||
When I press "Enable access"
|
||||
And I press "Continue"
|
||||
Then I should see "Recipients (2)"
|
||||
|
||||
Examples:
|
||||
| aggregationcriteria |
|
||||
| Any of the selected activities is complete |
|
||||
| All of the selected activities are complete |
|
||||
# Only student1 should earn the badge because student2 did not pass the quiz.
|
||||
Then I should see "Recipients (1)"
|
||||
And I select "Recipients (1)" from the "jump" singleselect
|
||||
And I should see "Student 1"
|
||||
And I should not see "Student 2"
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
@core @core_badges
|
||||
Feature: Delete course badge already awarded
|
||||
As a teacher
|
||||
I can delete awarded course badge
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
| student1 | Student | 1 | student1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
|
||||
@javascript
|
||||
Scenario Outline: Delete course badge already awarded to student
|
||||
Given the following "core_badges > Badges" exist:
|
||||
| name | course | description | image | status | type |
|
||||
| <badgename> | C1 | <badgename> description | badges/tests/behat/badge.png | active | 2 |
|
||||
And the following "core_badges > Criterias" exist:
|
||||
| badge | role |
|
||||
| <badgename> | editingteacher |
|
||||
And the following "core_badges > Issued badges" exist:
|
||||
| badge | user |
|
||||
| <badgename> | student1 |
|
||||
When I am on the "Course 1" "enrolled users" page logged in as "teacher1"
|
||||
And I click on "Student 1" "link"
|
||||
Then I should see "<badgename>"
|
||||
And I am on the "Course 1" course page
|
||||
# Navigate to Manage Badges page in order to delete the badge
|
||||
And I navigate to "Badges > Manage badges" in current page administration
|
||||
# Delete the badge
|
||||
And I click on "Delete" "icon" in the "<badgename>" "table_row"
|
||||
And I press "<deleteoption>"
|
||||
And I am on the "Course 1" "enrolled users" page
|
||||
And I click on "Student 1" "link"
|
||||
# Confirm that Badge 1 is retained as awarded badge but Badge 2 is not
|
||||
And I <visibility> see "<badgename>"
|
||||
And I am on the "Course 1" course page
|
||||
# Navigate to Badges page to confirm that no badges exist, hence, Manage badges would not exist
|
||||
And I navigate to "Badges" in current page administration
|
||||
# Confirm that badges are sucessfully deleted
|
||||
And I should see "There are currently no badges available for users to earn."
|
||||
|
||||
Examples:
|
||||
| badgename | deleteoption | visibility |
|
||||
| Badge 1 | Delete and keep existing issued badges | should |
|
||||
| Badge 2 | Delete and remove existing issued badges | should not |
|
||||
+20
-1
@@ -59,7 +59,26 @@ function block_comments_comment_validate($comment_param) {
|
||||
* @return array
|
||||
*/
|
||||
function block_comments_comment_permissions($args) {
|
||||
return array('post'=>true, 'view'=>true);
|
||||
global $DB, $USER;
|
||||
// By default, anyone can post and view comments.
|
||||
$canpost = $canview = true;
|
||||
// Check if it's the user context and not the owner's profile.
|
||||
if ($args->context->contextlevel == CONTEXT_USER && $USER->id != $args->context->instanceid) {
|
||||
// Check whether the context owner has a comment block in the user's profile.
|
||||
$sqlparam = [
|
||||
'blockname' => 'comments',
|
||||
'parentcontextid' => $args->context->id,
|
||||
'pagetypepattern' => 'user-profile',
|
||||
];
|
||||
// If the comment block is not present at the target user's profile,
|
||||
// then the logged-in user cannot post or view comments.
|
||||
$canpost = $canview = $DB->record_exists_select(
|
||||
'block_instances',
|
||||
'blockname = :blockname AND parentcontextid = :parentcontextid AND pagetypepattern = :pagetypepattern',
|
||||
$sqlparam,
|
||||
);
|
||||
}
|
||||
return ['post' => $canpost, 'view' => $canview];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,7 @@ Feature: Add a comment to the comments block
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | Frist | teacher1@example.com |
|
||||
| teacher1 | Teacher | First | teacher1@example.com |
|
||||
| student1 | Student | First | student1@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
@@ -25,11 +25,16 @@ Feature: Add a comment to the comments block
|
||||
Scenario: Add a comment with Javascript enabled
|
||||
When I add "I'm a comment from student1" comment to comments block
|
||||
Then I should see "I'm a comment from student1"
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
And I should see "I'm a comment from student1"
|
||||
|
||||
Scenario: Add a comment with Javascript disabled
|
||||
When I follow "Show comments"
|
||||
And I add "I'm a comment from student1" comment to comments block
|
||||
Then I should see "I'm a comment from student1"
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
And I follow "Show comments"
|
||||
And I should see "I'm a comment from student1"
|
||||
|
||||
@javascript
|
||||
Scenario: Test comment block pagination
|
||||
|
||||
@@ -10,7 +10,7 @@ Feature: Enable Block comments on an activity page and view comments
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | Frist | teacher1@example.com |
|
||||
| teacher1 | Teacher | First | teacher1@example.com |
|
||||
| student1 | Student | First | student1@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
@block @block_comments
|
||||
Feature: Enable Block comments on a course page and view comments
|
||||
In order to enable the comments block on a course page
|
||||
As a teacher
|
||||
I can add the comments block to the course page
|
||||
|
||||
Scenario: Add the comments block on the course page and add comments
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | Frist | teacher1@example.com |
|
||||
| student1 | Student | First | student1@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
And the following "blocks" exist:
|
||||
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
|
||||
| comments | Course | C1 | course-view-* | side-pre |
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
And I follow "Show comments"
|
||||
And I add "I'm a comment from the teacher" comment to comments block
|
||||
When I am on the "Course 1" course page logged in as student1
|
||||
And I follow "Show comments"
|
||||
Then I should see "I'm a comment from the teacher"
|
||||
@@ -10,7 +10,7 @@ Feature: Enable Block comments on the dashboard and view comments
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | Frist | teacher1@example.com |
|
||||
| teacher1 | Teacher | First | teacher1@example.com |
|
||||
|
||||
Scenario: Add the comments block on the dashboard and add comments with Javascript disabled
|
||||
When I log in as "teacher1"
|
||||
|
||||
+28
-76
@@ -1,4 +1,4 @@
|
||||
@block @block_completionstatus
|
||||
@block @block_completionstatus @core_completion
|
||||
Feature: Enable Block Completion in a course using activity completion
|
||||
In order to view the completion block in a course
|
||||
As a teacher
|
||||
@@ -17,117 +17,69 @@ Feature: Enable Block Completion in a course using activity completion
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
And the following "activities" exist:
|
||||
| activity | course | idnumber | name | intro |
|
||||
| page | C1 | page1 | Test page name | Test page description |
|
||||
| assign | C1 | assign1 | Test assign name | Test page description |
|
||||
| activity | course | idnumber | name | gradepass | completion | completionview | completionusegrade | completionpassgrade |
|
||||
| page | C1 | page1 | Test page name | | 2 | 1 | 0 | 0 |
|
||||
| assign | C1 | assign1 | Test assign name | 50 | 2 | 0 | 1 | 1 |
|
||||
And the following "blocks" exist:
|
||||
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
|
||||
| completionstatus | Course | C1 | course-view-* | side-pre |
|
||||
|
||||
Scenario: Add the block to a the course and add course completion items
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I follow "Test page name"
|
||||
And I navigate to "Settings" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Completion tracking | Show activity as complete when conditions are met |
|
||||
| Require view | 1 |
|
||||
And I press "Save and return to course"
|
||||
And I add the "Course completion status" block
|
||||
Scenario: Completion status block when student has not started any activities
|
||||
Given I am on the "Course 1" course page logged in as teacher1
|
||||
And I navigate to "Course completion" in current page administration
|
||||
And I expand all fieldsets
|
||||
And I set the following fields to these values:
|
||||
| Test page name | 1 |
|
||||
And I press "Save changes"
|
||||
And I log out
|
||||
When I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
When I am on the "Course 1" course page logged in as student1
|
||||
Then I should see "Status: Not yet started" in the "Course completion status" "block"
|
||||
And I should see "0 of 1" in the "Activity completion" "table_row"
|
||||
|
||||
Scenario: Add the block to a the course and add course completion items
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I follow "Test page name"
|
||||
And I navigate to "Settings" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Completion tracking | Show activity as complete when conditions are met |
|
||||
| Require view | 1 |
|
||||
And I press "Save and return to course"
|
||||
And I add the "Course completion status" block
|
||||
Scenario: Completion status block when student has completed a page
|
||||
Given I am on the "Course 1" course page logged in as teacher1
|
||||
And I navigate to "Course completion" in current page administration
|
||||
And I expand all fieldsets
|
||||
And I set the following fields to these values:
|
||||
| Test page name | 1 |
|
||||
And I press "Save changes"
|
||||
And I log out
|
||||
When I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "Test page name"
|
||||
When I am on the "Test page name" "page activity" page logged in as student1
|
||||
And I am on "Course 1" course homepage
|
||||
Then I should see "Status: Complete" in the "Course completion status" "block"
|
||||
And I should see "1 of 1" in the "Activity completion" "table_row"
|
||||
And I follow "More details"
|
||||
And I should see "Yes" in the "Activity completion" "table_row"
|
||||
|
||||
@javascript
|
||||
Scenario: Add the block to a the course and add course completion items with passing grade
|
||||
Given I am on the "Test assign name" "assign activity" page logged in as teacher1
|
||||
And I navigate to "Settings" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Completion tracking | Show activity as complete when conditions are met |
|
||||
| completionusegrade | 1 |
|
||||
| completionpassgrade | 1 |
|
||||
| gradepass | 50 |
|
||||
And I press "Save and return to course"
|
||||
And I am on the "Test assign name" "assign activity" page
|
||||
And I follow "View all submissions"
|
||||
And I click on "Grade" "link" in the "Student" "table_row"
|
||||
And I set the field "Grade out of 100" to "53"
|
||||
And I set the field "Notify student" to "0"
|
||||
And I press "Save changes"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add the "Course completion status" block
|
||||
Scenario: Completion status block with items with passing grade
|
||||
Given I am on the "Course 1" course page logged in as teacher1
|
||||
And I navigate to "Course completion" in current page administration
|
||||
And I expand all fieldsets
|
||||
And I set the following fields to these values:
|
||||
| Test assign name | 1 |
|
||||
And I press "Save changes"
|
||||
And I log out
|
||||
When I am on the "Test assign name" "assign activity" page logged in as student1
|
||||
And I am on "Course 1" course homepage
|
||||
Then I should see "Status: Pending" in the "Course completion status" "block"
|
||||
And I should see "0 of 1" in the "Activity completion" "table_row"
|
||||
And the following "grade grades" exist:
|
||||
| gradeitem | user | grade |
|
||||
| Test assign name | student1 | 53 |
|
||||
When I am on the "Course 1" course page logged in as student1
|
||||
Then I should see "Status: Complete" in the "Course completion status" "block"
|
||||
And I should see "1 of 1" in the "Activity completion" "table_row"
|
||||
And I trigger cron
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "More details"
|
||||
And I should see "Achieving grade, Achieving passing grade" in the "Activity completion" "table_row"
|
||||
And I should see "Yes" in the "Activity completion" "table_row"
|
||||
|
||||
@javascript
|
||||
Scenario: Add the block to a the course and add course completion items with failing grade.
|
||||
Given I am on the "Test assign name" "assign activity" page logged in as teacher1
|
||||
And I navigate to "Settings" in current page administration
|
||||
And I set the following fields to these values:
|
||||
| Completion tracking | Show activity as complete when conditions are met |
|
||||
| completionusegrade | 1 |
|
||||
| completionpassgrade | 1 |
|
||||
| gradepass | 50 |
|
||||
And I press "Save and return to course"
|
||||
And I am on the "Test assign name" "assign activity" page
|
||||
And I follow "View all submissions"
|
||||
And I click on "Grade" "link" in the "Student" "table_row"
|
||||
And I set the field "Grade out of 100" to "49"
|
||||
And I set the field "Notify student" to "0"
|
||||
And I press "Save changes"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add the "Course completion status" block
|
||||
Scenario: Completion status block with items with failing grade
|
||||
Given I am on the "Course 1" course page logged in as teacher1
|
||||
And the following "grade grades" exist:
|
||||
| gradeitem | user | grade |
|
||||
| Test assign name | student1 | 49 |
|
||||
And I navigate to "Course completion" in current page administration
|
||||
And I expand all fieldsets
|
||||
And I set the following fields to these values:
|
||||
| Test assign name | 1 |
|
||||
And I press "Save changes"
|
||||
And I log out
|
||||
When I am on the "Test assign name" "assign activity" page logged in as student1
|
||||
And I am on "Course 1" course homepage
|
||||
Then I should see "Status: Pending" in the "Course completion status" "block"
|
||||
When I am on the "Course 1" course page logged in as student1
|
||||
Then I should see "Status: Not yet started" in the "Course completion status" "block"
|
||||
And I should see "0 of 1" in the "Activity completion" "table_row"
|
||||
And I trigger cron
|
||||
And I am on "Course 1" course homepage
|
||||
|
||||
@@ -30,7 +30,7 @@ $string['aria:allcoursesincludinghidden'] = 'Show all courses';
|
||||
$string['aria:allcourses'] = 'Show all courses except courses removed from view';
|
||||
$string['aria:card'] = 'Switch to card view';
|
||||
$string['aria:controls'] = 'Course overview controls';
|
||||
$string['aria:courseactions'] = 'Actions for current course';
|
||||
$string['aria:courseactions'] = 'Actions for course';
|
||||
$string['aria:coursesummary'] = 'Course summary text:';
|
||||
$string['aria:courseprogress'] = 'Course progress:';
|
||||
$string['aria:customfield'] = 'Show {$a} courses';
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"name": "Assignment due 1",
|
||||
"viewurl": "https://moodlesite/course/view.php?id=2",
|
||||
"courseimage": "https://moodlesite/pluginfile/123/course/overviewfiles/123.jpg",
|
||||
"fullname": "course 3",
|
||||
"fullname": "Course 3 for \"Statistical and Computational Tools\" ",
|
||||
"hasprogress": true,
|
||||
"progress": 10,
|
||||
"coursecategory": "Category 1",
|
||||
@@ -50,8 +50,11 @@
|
||||
{{/hasprogress}}
|
||||
{{/progress}}
|
||||
{{$coursename}}
|
||||
<span class="multiline">
|
||||
{{#shortentext}}50, {{{fullname}}} {{/shortentext}}
|
||||
<span class="multiline" title="{{fullname}}">
|
||||
<span class="sr-only">{{{fullname}}}</span>
|
||||
<span aria-hidden="true">
|
||||
{{#shortentext}}55, {{{fullname}}} {{/shortentext}}
|
||||
</span>
|
||||
</span>
|
||||
{{/coursename}}
|
||||
{{$coursecategory}}
|
||||
|
||||
@@ -94,6 +94,7 @@ Feature: The my overview block allows users to hide their courses
|
||||
When I click on "All" "link" in the "Course overview" "block"
|
||||
And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
|
||||
And I click on "Remove from view" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
|
||||
And I wait "1" seconds
|
||||
Then I should not see "Course 2" in the "Course overview" "block"
|
||||
|
||||
Scenario: Test a course is never hidden with "All (including removed from view)" courses
|
||||
|
||||
+3
-3
@@ -86,7 +86,7 @@ class recentlyaccesseditems_item_exporter extends \core\external\exporter {
|
||||
'type' => PARAM_INT,
|
||||
),
|
||||
'modname' => array(
|
||||
'type' => PARAM_TEXT,
|
||||
'type' => PARAM_PLUGIN,
|
||||
),
|
||||
'name' => array(
|
||||
'type' => PARAM_TEXT,
|
||||
@@ -108,7 +108,7 @@ class recentlyaccesseditems_item_exporter extends \core\external\exporter {
|
||||
public static function define_other_properties() {
|
||||
return array(
|
||||
'viewurl' => array(
|
||||
'type' => PARAM_TEXT,
|
||||
'type' => PARAM_RAW,
|
||||
),
|
||||
'courseviewurl' => array(
|
||||
'type' => PARAM_URL,
|
||||
@@ -117,7 +117,7 @@ class recentlyaccesseditems_item_exporter extends \core\external\exporter {
|
||||
'type' => PARAM_RAW,
|
||||
),
|
||||
'purpose' => array(
|
||||
'type' => PARAM_TEXT,
|
||||
'type' => PARAM_ALPHA,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Vendored
+14
-4
@@ -441,8 +441,17 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
|
||||
} while (!feof($handle));
|
||||
$this->lastiobytes = strlen($data);
|
||||
|
||||
if ($this->lastiobytes == 0) {
|
||||
// Potentially statcache is stale. File can be deleted, let's clear cache and recheck.
|
||||
clearstatcache(true, $file);
|
||||
if (!file_exists($file)) {
|
||||
// It's a completely normal condition. Just ignore and keep going.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Return it unserialised.
|
||||
return $this->prep_data_after_read($data);
|
||||
return $this->prep_data_after_read($data, $file);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -548,13 +557,14 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
|
||||
* Prepares the data it has been read from the cache. Undoing what was done in prep_data_before_save.
|
||||
*
|
||||
* @param string $data
|
||||
* @param string $path
|
||||
* @return mixed
|
||||
* @throws coding_exception
|
||||
*/
|
||||
protected function prep_data_after_read($data) {
|
||||
protected function prep_data_after_read($data, $path) {
|
||||
$result = @unserialize($data);
|
||||
if ($result === false && $data != serialize(false)) {
|
||||
throw new coding_exception('Failed to unserialise data from file. Either failed to read, or failed to write.');
|
||||
debugging('Failed to unserialise data from cache file: ' . $path . '. Data: ' . $data, DEBUG_DEVELOPER);
|
||||
return false;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
}} data-month="{{previousperiod.mon}}"{{!
|
||||
}} data-day="{{previousperiod.mday}}"{{!
|
||||
}}>
|
||||
<span class="arrow">{{{larrow}}}</span>
|
||||
<span class="arrow" aria-hidden="true">{{{larrow}}}</span>
|
||||
|
||||
<span class="arrow_text">{{previousperiodname}}</span>
|
||||
</a>
|
||||
@@ -58,7 +58,7 @@
|
||||
}}>
|
||||
<span class="arrow_text">{{nextperiodname}}</span>
|
||||
|
||||
<span class="arrow">{{{rarrow}}}</span>
|
||||
<span class="arrow" aria-hidden="true">{{{rarrow}}}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
}} data-month="{{previousperiod.mon}}"{{!
|
||||
}} data-drop-zone="nav-link" {{!
|
||||
}}>
|
||||
<span class="arrow">{{{larrow}}}</span>
|
||||
<span class="arrow" aria-hidden="true">{{{larrow}}}</span>
|
||||
|
||||
<span class="arrow_text">{{previousperiodname}}</span>
|
||||
</a>
|
||||
@@ -63,7 +63,7 @@
|
||||
}}>
|
||||
<span class="arrow_text">{{nextperiodname}}</span>
|
||||
|
||||
<span class="arrow">{{{rarrow}}}</span>
|
||||
<span class="arrow" aria-hidden="true">{{{rarrow}}}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -176,7 +176,23 @@ class behat_calendar extends behat_base {
|
||||
* @return void
|
||||
*/
|
||||
public function i_am_viewing_site_calendar() {
|
||||
$url = new moodle_url('/calendar/view.php', ['view' => 'month']);
|
||||
$this->i_am_viewing_calendar_in_view('month');
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a specific view in the calendar.
|
||||
*
|
||||
* @Given /^I am viewing calendar in "([^"]+)" view$/
|
||||
* @param string $view The calendar view ('month', 'day' and 'upcoming') to navigate to.
|
||||
* @return void
|
||||
*/
|
||||
public function i_am_viewing_calendar_in_view(string $view): void {
|
||||
|
||||
if (!in_array($view, ['month', 'day', 'upcoming'])) {
|
||||
throw new Exception("Invalid calendar view. Allowed values are: 'month', 'day' and 'upcoming'");
|
||||
}
|
||||
|
||||
$url = new moodle_url('/calendar/view.php', ['view' => $view]);
|
||||
$this->execute('behat_general::i_visit', [$url]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,11 @@ M.core_comment = {
|
||||
this.courseid = args.courseid;
|
||||
this.contextid = args.contextid;
|
||||
this.autostart = (args.autostart);
|
||||
// Fail fast if the comments element cannot be found, such as in embedded-type views where blocks may be loaded
|
||||
// then discarded.
|
||||
if (!Y.one('#comment-ctrl-'+this.client_id)) {
|
||||
return;
|
||||
}
|
||||
// expand comments?
|
||||
if (this.autostart) {
|
||||
this.view(args.page);
|
||||
|
||||
@@ -155,8 +155,8 @@ class completion_criteria_activity extends completion_criteria {
|
||||
|
||||
$data = $info->get_data($cm, false, $completion->userid);
|
||||
|
||||
// If the activity is complete
|
||||
if (in_array($data->completionstate, array(COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS, COMPLETION_COMPLETE_FAIL))) {
|
||||
// If the activity is complete.
|
||||
if (in_array($data->completionstate, [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS])) {
|
||||
if ($mark) {
|
||||
$completion->mark_complete();
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ Feature: Allow teachers to bulk edit activity completion rules in a course.
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | Frist | teacher1@example.com |
|
||||
| teacher1 | Teacher | First | teacher1@example.com |
|
||||
| student1 | Student | First | student1@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@block @block_completionstatus @javascript
|
||||
@block @block_completionstatus @core_completion @javascript
|
||||
Feature: Course completion state should match completion criteria
|
||||
In order to understand the configuration or status of an course's completion
|
||||
As a user
|
||||
@@ -8,7 +8,6 @@ Feature: Course completion state should match completion criteria
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email | idnumber |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com | T1 |
|
||||
| teacher2 | Teacher | 2 | teacher1@example.com | T2 |
|
||||
| student1 | Student | 1 | student1@example.com | S1 |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category | enablecompletion | showcompletionconditions |
|
||||
@@ -16,16 +15,18 @@ Feature: Course completion state should match completion criteria
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| teacher2 | C1 | teacher |
|
||||
| student1 | C1 | student |
|
||||
And the following "activity" exists:
|
||||
| activity | assign |
|
||||
| course | C1 |
|
||||
| name | Test assignment name |
|
||||
| completion | 1 |
|
||||
| assignsubmission_onlinetext_enabled | 1 |
|
||||
| grade[modgrade_type] | Point |
|
||||
| grade[modgrade_point] | 100 |
|
||||
| grade[modgrade_type] | Point |
|
||||
| grade[modgrade_point] | 100 |
|
||||
| gradepass | 70 |
|
||||
| completion | 2 |
|
||||
| completionusegrade | 1 |
|
||||
| completionpassgrade | 1 |
|
||||
And the following "blocks" exist:
|
||||
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
|
||||
| completionstatus | Course | C1 | course-view-* | side-pre |
|
||||
@@ -34,58 +35,36 @@ Feature: Course completion state should match completion criteria
|
||||
And I click on "Condition: Activity completion" "link"
|
||||
And I set the field "Assignment - Test assignment name" to "1"
|
||||
And I press "Save changes"
|
||||
And I am on the "Test assignment name" "assign activity editing" page
|
||||
And I set the following fields to these values:
|
||||
| Completion tracking | Show activity as complete when conditions are met |
|
||||
| completionusegrade | 1 |
|
||||
| completionpassgrade | 1 |
|
||||
| gradepass | 70 |
|
||||
And I press "Save and return to course"
|
||||
|
||||
Scenario: Completion status show match completion criteria when passgrage condition is set.
|
||||
Given I am on the "Course 1" course page logged in as "student1"
|
||||
And the "Receive a grade" completion condition of "Test assignment name" is displayed as "todo"
|
||||
And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "todo"
|
||||
And I should see "Status: Not yet started" in the "Course completion status" "block"
|
||||
And I am on the "Test assignment name" "assign activity" page
|
||||
And I press "Add submission"
|
||||
And I set the following fields to these values:
|
||||
| Online text | I'm the student1 submission |
|
||||
And I press "Save changes"
|
||||
And I press "Submit assignment"
|
||||
And I press "Continue"
|
||||
And I am on the "Test assignment name" "assign activity" page logged in as teacher1
|
||||
And I follow "View all submissions"
|
||||
And I click on "Grade" "link" in the "Student 1" "table_row"
|
||||
And I set the following fields to these values:
|
||||
| Grade out of 100 | 50.0 |
|
||||
And I press "Save changes"
|
||||
And I am on the "Course 1" course page
|
||||
And I navigate to "Reports" in current page administration
|
||||
And I click on "Activity completion" "link"
|
||||
And "Student 1, Test assignment name: Completed (did not achieve pass grade)" "icon" should exist in the "Student 1" "table_row"
|
||||
And I navigate to "Reports" in current page administration
|
||||
And I click on "Course completion" "link" in the "region-main" "region"
|
||||
And "Student 1, Test assignment name: Completed (did not achieve pass grade)" "icon" should exist in the "Student 1" "table_row"
|
||||
And "Student 1, Course complete: Not completed" "icon" should exist in the "Student 1" "table_row"
|
||||
When I am on the "Course 1" course page logged in as "student1"
|
||||
And I should see "Status: Pending" in the "Course completion status" "block"
|
||||
When the following "mod_assign > submissions" exist:
|
||||
| assign | user | onlinetext |
|
||||
| Test assignment name | student1 | This is a submission for assignment |
|
||||
And the following "grade grades" exist:
|
||||
| gradeitem | user | grade |
|
||||
| Test assignment name | student1 | 50 |
|
||||
And I reload the page
|
||||
Then I should see "Status: Not yet started" in the "Course completion status" "block"
|
||||
And the "Receive a grade" completion condition of "Test assignment name" is displayed as "done"
|
||||
And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "failed"
|
||||
And I am on the "My courses" page
|
||||
And I should not see "100%" in the "Course overview" "block"
|
||||
And I am on the "Test assignment name" "assign activity" page logged in as teacher1
|
||||
And I follow "View all submissions"
|
||||
And I click on "Grade" "link" in the "Student 1" "table_row"
|
||||
And I set the following fields to these values:
|
||||
| Grade out of 100 | 75.0 |
|
||||
And I press "Save changes"
|
||||
And I am on the "Course 1" course page
|
||||
And I navigate to "Reports" in current page administration
|
||||
And I click on "Activity completion" "link"
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
And I navigate to "Reports > Activity completion" in current page administration
|
||||
And "Student 1, Test assignment name: Completed (did not achieve pass grade)" "icon" should exist in the "Student 1" "table_row"
|
||||
And I navigate to "Reports > Course completion" in current page administration
|
||||
And "Student 1, Test assignment name: Completed (did not achieve pass grade)" "icon" should exist in the "Student 1" "table_row"
|
||||
And "Student 1, Course complete: Not completed" "icon" should exist in the "Student 1" "table_row"
|
||||
And the following "grade grades" exist:
|
||||
| gradeitem | user | grade |
|
||||
| Test assignment name | student1 | 75 |
|
||||
And I navigate to "Reports > Activity completion" in current page administration
|
||||
And "Student 1, Test assignment name: Completed (achieved pass grade)" "icon" should exist in the "Student 1" "table_row"
|
||||
And I navigate to "Reports" in current page administration
|
||||
And I click on "Course completion" "link" in the "region-main" "region"
|
||||
And I navigate to "Reports > Course completion" in current page administration
|
||||
And "Student 1, Test assignment name: Completed (achieved pass grade)" "icon" should exist in the "Student 1" "table_row"
|
||||
And "Student 1, Course complete: Completed" "icon" should exist in the "Student 1" "table_row"
|
||||
And I am on the "Course 1" course page logged in as "student1"
|
||||
@@ -93,62 +72,46 @@ Feature: Course completion state should match completion criteria
|
||||
And the "Receive a grade" completion condition of "Test assignment name" is displayed as "done"
|
||||
And the "Receive a passing grade" completion condition of "Test assignment name" is displayed as "done"
|
||||
And I am on the "My courses" page
|
||||
Then I should see "100%" in the "Course overview" "block"
|
||||
And I should see "100%" in the "Course overview" "block"
|
||||
|
||||
Scenario: Completion status show match completion criteria when passgrage condition is not set.
|
||||
Given I am on the "Test assignment name" "assign activity editing" page logged in as teacher1
|
||||
And I set the following fields to these values:
|
||||
| Completion tracking | Show activity as complete when conditions are met |
|
||||
| completionusegrade | 1 |
|
||||
| completionpassgrade | 0 |
|
||||
| gradepass | 70 |
|
||||
| completionpassgrade | 0 |
|
||||
And I press "Save and return to course"
|
||||
And I am on the "Course 1" course page logged in as "student1"
|
||||
And the "Receive a grade" completion condition of "Test assignment name" is displayed as "todo"
|
||||
And I should see "Status: Not yet started" in the "Course completion status" "block"
|
||||
And I am on the "Test assignment name" "assign activity" page
|
||||
And I press "Add submission"
|
||||
And I set the following fields to these values:
|
||||
| Online text | I'm the student1 submission |
|
||||
And I press "Save changes"
|
||||
And I press "Submit assignment"
|
||||
And I press "Continue"
|
||||
And I am on the "Test assignment name" "assign activity" page logged in as teacher1
|
||||
And I follow "View all submissions"
|
||||
And I click on "Grade" "link" in the "Student 1" "table_row"
|
||||
And I set the following fields to these values:
|
||||
| Grade out of 100 | 50.0 |
|
||||
And I press "Save changes"
|
||||
And I am on the "Course 1" course page
|
||||
And I navigate to "Reports" in current page administration
|
||||
And I click on "Activity completion" "link"
|
||||
And "Student 1, Test assignment name: Completed (did not achieve pass grade)" "icon" should exist in the "Student 1" "table_row"
|
||||
And I navigate to "Reports" in current page administration
|
||||
And I click on "Course completion" "link" in the "region-main" "region"
|
||||
And "Student 1, Test assignment name: Completed (did not achieve pass grade)" "icon" should exist in the "Student 1" "table_row"
|
||||
And "Student 1, Course complete: Completed" "icon" should exist in the "Student 1" "table_row"
|
||||
When I am on the "Course 1" course page logged in as "student1"
|
||||
And I should see "Status: Complete" in the "Course completion status" "block"
|
||||
When the following "mod_assign > submissions" exist:
|
||||
| assign | user | onlinetext |
|
||||
| Test assignment name | student1 | I'm the student1 submission |
|
||||
And the following "grade grades" exist:
|
||||
| gradeitem | user | grade |
|
||||
| Test assignment name | student1 | 50 |
|
||||
And I reload the page
|
||||
# TODO: Expected status is Complete but activity is marked as completed with a failed icon.
|
||||
# Then I should see "Status: Complete" in the "Course completion status" "block"
|
||||
Then I should see "Status: Pending" in the "Course completion status" "block"
|
||||
# Once MDL-75582 is fixed "failed" should be changed to "done"
|
||||
And the "Receive a grade" completion condition of "Test assignment name" is displayed as "failed"
|
||||
And I am on the "My courses" page
|
||||
And I should see "100%" in the "Course overview" "block"
|
||||
And I am on the "Test assignment name" "assign activity" page logged in as teacher1
|
||||
And I follow "View all submissions"
|
||||
And I click on "Grade" "link" in the "Student 1" "table_row"
|
||||
And I set the following fields to these values:
|
||||
| Grade out of 100 | 75.0 |
|
||||
And I press "Save changes"
|
||||
And I am on the "Course 1" course page
|
||||
And I navigate to "Reports" in current page administration
|
||||
And I click on "Activity completion" "link"
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
And I navigate to "Reports > Activity completion" in current page administration
|
||||
And "Student 1, Test assignment name: Completed (did not achieve pass grade)" "icon" should exist in the "Student 1" "table_row"
|
||||
And I navigate to "Reports > Course completion" in current page administration
|
||||
And "Student 1, Test assignment name: Completed (did not achieve pass grade)" "icon" should exist in the "Student 1" "table_row"
|
||||
And "Student 1, Course complete: Completed" "icon" should exist in the "Student 1" "table_row"
|
||||
And the following "grade grades" exist:
|
||||
| gradeitem | user | grade |
|
||||
| Test assignment name | student1 | 75 |
|
||||
And I navigate to "Reports > Activity completion" in current page administration
|
||||
And "Student 1, Test assignment name: Completed (achieved pass grade)" "icon" should exist in the "Student 1" "table_row"
|
||||
And I navigate to "Reports" in current page administration
|
||||
And I click on "Course completion" "link" in the "region-main" "region"
|
||||
And I navigate to "Reports > Course completion" in current page administration
|
||||
And "Student 1, Test assignment name: Completed (achieved pass grade)" "icon" should exist in the "Student 1" "table_row"
|
||||
And "Student 1, Course complete: Completed" "icon" should exist in the "Student 1" "table_row"
|
||||
And I am on the "Course 1" course page logged in as "student1"
|
||||
And I should see "Status: Complete" in the "Course completion status" "block"
|
||||
And the "Receive a grade" completion condition of "Test assignment name" is displayed as "done"
|
||||
And I am on the "My courses" page
|
||||
Then I should see "100%" in the "Course overview" "block"
|
||||
And I should see "100%" in the "Course overview" "block"
|
||||
|
||||
@@ -14,7 +14,7 @@ Feature: Allow teachers to edit the default activity completion rules in a cours
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | Frist | teacher1@example.com |
|
||||
| teacher1 | Teacher | First | teacher1@example.com |
|
||||
| student1 | Student | First | student1@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
@@ -48,3 +48,37 @@ Feature: Allow teachers to edit the default activity completion rules in a cours
|
||||
And I should see "Student must receive a grade to complete this activity" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Assignments']]" "xpath_element"
|
||||
And I should see "Student must make a submission" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Assignments']]" "xpath_element"
|
||||
And I should not see "Completion expected on" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' row ')][.//*[text() = 'Assignments']]" "xpath_element"
|
||||
|
||||
@javascript
|
||||
Scenario: Course activity completion default rules for SCORM
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category | enablecompletion |
|
||||
| Course 1 | C1 | 0 | 1 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | First | teacher1@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
When I navigate to "Course completion" in current page administration
|
||||
And I select "Default activity completion" from the "Course completion tertiary navigation" singleselect
|
||||
And I click on "SCORM packages" "checkbox"
|
||||
And I click on "Edit" "button"
|
||||
# Check Passed option.
|
||||
And I set the following fields to these values:
|
||||
| completion | Show activity as complete when conditions are met|
|
||||
| completionstatusrequired[2] | 1 |
|
||||
And I click on "Save changes" "button"
|
||||
Then I should see "Changes saved"
|
||||
And I should see "Student must achieve at least one of the following statuses: Passed, Completed"
|
||||
And I navigate to "Course completion" in current page administration
|
||||
And I select "Default activity completion" from the "Course completion tertiary navigation" singleselect
|
||||
And I click on "SCORM packages" "checkbox"
|
||||
And I click on "Edit" "button"
|
||||
# Uncheck Pass option.
|
||||
And I set the following fields to these values:
|
||||
| completionstatusrequired[2] | 0 |
|
||||
And I click on "Save changes" "button"
|
||||
Then I should see "Changes saved"
|
||||
And I should see "Student must achieve at least one of the following statuses: Completed"
|
||||
|
||||
@@ -7,7 +7,7 @@ Feature: Students will be marked as completed if they have achieved a passing gr
|
||||
| Course 1 | C1 | 0 | 1 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | Frist | teacher1@example.com |
|
||||
| teacher1 | Teacher | First | teacher1@example.com |
|
||||
| student1 | Student | First | student1@example.com |
|
||||
| student2 | Student | Second | student2@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
|
||||
@@ -8,7 +8,7 @@ Feature: Students will be marked as completed and pass/fail
|
||||
| Course 1 | C1 | 0 | 1 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | Frist | teacher1@example.com |
|
||||
| teacher1 | Teacher | First | teacher1@example.com |
|
||||
| student1 | Student | First | student1@example.com |
|
||||
| student2 | Student | Second | student2@example.com |
|
||||
| student3 | Student | Third | student3@example.com |
|
||||
|
||||
@@ -11,7 +11,7 @@ Feature: Allow students to manually mark an activity as complete
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | Frist | teacher1@example.com |
|
||||
| teacher1 | Teacher | First | teacher1@example.com |
|
||||
| student1 | Student | First | student1@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
|
||||
@@ -10,7 +10,7 @@ Feature: Students will be shown relevant completion state based on grade item vi
|
||||
| Course 1 | C1 | 0 | 1 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | Frist | teacher1@example.com |
|
||||
| teacher1 | Teacher | First | teacher1@example.com |
|
||||
| student1 | Student | First | student1@example.com |
|
||||
| student2 | Student | Second | student2@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
|
||||
@@ -10,7 +10,7 @@ Feature: Restrict activity availability through date conditions
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | Frist | teacher1@example.com |
|
||||
| teacher1 | Teacher | First | teacher1@example.com |
|
||||
| student1 | Student | First | student1@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
|
||||
@@ -11,7 +11,7 @@ Feature: Restrict activity availability through grade conditions
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | Frist | teacher1@example.com |
|
||||
| teacher1 | Teacher | First | teacher1@example.com |
|
||||
| student1 | Student | First | student1@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
|
||||
@@ -10,7 +10,7 @@ Feature: Restrict sections availability through completion or grade conditions
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | Frist | teacher1@example.com |
|
||||
| teacher1 | Teacher | First | teacher1@example.com |
|
||||
| student1 | Student | First | student1@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
|
||||
Vendored
+1
-1
@@ -5,6 +5,6 @@ define("core_course/repository",["exports","core/ajax"],(function(_exports,_ajax
|
||||
* @module core_course/repository
|
||||
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};var _default={getEnrolledCoursesByTimelineClassification:(classification,limit,offset,sort)=>{const args={classification:classification};void 0!==limit&&(args.limit=limit),void 0!==offset&&(args.offset=offset),void 0!==sort&&(args.sort=sort);const request={methodname:"core_course_get_enrolled_courses_by_timeline_classification",args:args};return _ajax.default.call([request])[0]},getLastAccessedCourses:(userid,limit,offset,sort)=>{const args={};void 0!==userid&&(args.userid=userid),void 0!==limit&&(args.limit=limit),void 0!==offset&&(args.offset=offset),void 0!==sort&&(args.sort=sort);const request={methodname:"core_course_get_recent_courses",args:args};return _ajax.default.call([request])[0]},getUsersFromCourseModuleID:function(cmid,groupID){let onlyActive=arguments.length>2&&void 0!==arguments[2]&&arguments[2];var request={methodname:"core_course_get_enrolled_users_by_cmid",args:{cmid:cmid,groupid:groupID,onlyactive:onlyActive}};return _ajax.default.call([request])[0]},toggleManualCompletion:(cmid,completed)=>{const request={methodname:"core_completion_update_activity_completion_status_manually",args:{cmid:cmid,completed:completed}};return _ajax.default.call([request])[0]},getEnrolledCoursesWithEventsByTimelineClassification:function(classification){let limit=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,offset=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,sort=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null,searchValue=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null,eventsFrom=arguments.length>5&&void 0!==arguments[5]?arguments[5]:null,eventsTo=arguments.length>6&&void 0!==arguments[6]?arguments[6]:null;const args={classification:classification,limit:limit,offset:offset,sort:sort,eventsfrom:eventsFrom,eventsto:eventsTo,searchvalue:searchValue},request={methodname:"core_course_get_enrolled_courses_with_action_events_by_timeline_classification",args:args};return _ajax.default.call([request])[0]}};return _exports.default=_default,_exports.default}));
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};var _default={getEnrolledCoursesByTimelineClassification:(classification,limit,offset,sort)=>{const args={classification:classification};void 0!==limit&&(args.limit=limit),void 0!==offset&&(args.offset=offset),void 0!==sort&&(args.sort=sort);const request={methodname:"core_course_get_enrolled_courses_by_timeline_classification",args:args};return _ajax.default.call([request])[0]},getLastAccessedCourses:(userid,limit,offset,sort)=>{const args={};void 0!==userid&&(args.userid=userid),void 0!==limit&&(args.limit=limit),void 0!==offset&&(args.offset=offset),void 0!==sort&&(args.sort=sort);const request={methodname:"core_course_get_recent_courses",args:args};return _ajax.default.call([request])[0]},getUsersFromCourseModuleID:function(cmid,groupID){let onlyActive=arguments.length>2&&void 0!==arguments[2]&&arguments[2];var request={methodname:"core_course_get_enrolled_users_by_cmid",args:{cmid:cmid,groupid:groupID,onlyactive:onlyActive}};return _ajax.default.call([request])[0]},getGradableUsersFromCourseID:function(courseid,groupID){let onlyActive=arguments.length>2&&void 0!==arguments[2]&&arguments[2];const request={methodname:"core_grades_get_gradable_users",args:{courseid:courseid,groupid:groupID,onlyactive:onlyActive}};return _ajax.default.call([request])[0]},toggleManualCompletion:(cmid,completed)=>{const request={methodname:"core_completion_update_activity_completion_status_manually",args:{cmid:cmid,completed:completed}};return _ajax.default.call([request])[0]},getEnrolledCoursesWithEventsByTimelineClassification:function(classification){let limit=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,offset=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,sort=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null,searchValue=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null,eventsFrom=arguments.length>5&&void 0!==arguments[5]?arguments[5]:null,eventsTo=arguments.length>6&&void 0!==arguments[6]?arguments[6]:null;const args={classification:classification,limit:limit,offset:offset,sort:sort,eventsfrom:eventsFrom,eventsto:eventsTo,searchvalue:searchValue},request={methodname:"core_course_get_enrolled_courses_with_action_events_by_timeline_classification",args:args};return _ajax.default.call([request])[0]}};return _exports.default=_default,_exports.default}));
|
||||
|
||||
//# sourceMappingURL=repository.min.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -150,6 +150,27 @@ const getEnrolledUsersFromCourseModuleID = (cmid, groupID, onlyActive = false) =
|
||||
return Ajax.call([request])[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the list of gradable users enrolled in this course.
|
||||
*
|
||||
* @param {Number} courseid Course ID from which the users will be obtained
|
||||
* @param {Number} groupID Group ID from which the users will be obtained
|
||||
* @param {Boolean} onlyActive Whether to fetch only the active enrolled users or all enrolled users in the course.
|
||||
* @returns {Promise} Promise containing a list of users
|
||||
*/
|
||||
const getGradabaleUsersFromCourseID = (courseid, groupID, onlyActive = false) => {
|
||||
const request = {
|
||||
methodname: 'core_grades_get_gradable_users',
|
||||
args: {
|
||||
courseid: courseid,
|
||||
groupid: groupID,
|
||||
onlyactive: onlyActive,
|
||||
},
|
||||
};
|
||||
|
||||
return Ajax.call([request])[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle the completion state of an activity with manual completion.
|
||||
*
|
||||
@@ -172,6 +193,7 @@ export default {
|
||||
getEnrolledCoursesByTimelineClassification,
|
||||
getLastAccessedCourses,
|
||||
getUsersFromCourseModuleID: getEnrolledUsersFromCourseModuleID,
|
||||
getGradableUsersFromCourseID: getGradabaleUsersFromCourseID,
|
||||
toggleManualCompletion,
|
||||
getEnrolledCoursesWithEventsByTimelineClassification,
|
||||
};
|
||||
|
||||
@@ -349,7 +349,11 @@ class core_course_list_element implements IteratorAggregate {
|
||||
* @return string
|
||||
*/
|
||||
public function get_formatted_name() {
|
||||
return format_string(get_course_display_name_for_list($this), true, $this->get_context());
|
||||
return format_string(
|
||||
get_course_display_name_for_list($this),
|
||||
true,
|
||||
['context' => $this->get_context()]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -357,7 +361,11 @@ class core_course_list_element implements IteratorAggregate {
|
||||
* @return string
|
||||
*/
|
||||
public function get_formatted_fullname() {
|
||||
return format_string($this->__get('fullname'), true, $this->get_context());
|
||||
return format_string(
|
||||
$this->__get('fullname'),
|
||||
true,
|
||||
['context' => $this->get_context()]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -365,7 +373,11 @@ class core_course_list_element implements IteratorAggregate {
|
||||
* @return string
|
||||
*/
|
||||
public function get_formatted_shortname() {
|
||||
return format_string($this->__get('shortname'), true, $this->get_context());
|
||||
return format_string(
|
||||
$this->__get('shortname'),
|
||||
true,
|
||||
['context' => $this->get_context()]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+18
-1
@@ -87,8 +87,25 @@ if (!async_helper::is_async_pending($id, 'course', 'backup')) {
|
||||
$message = "{$strdeletecoursecheck}<br /><br />{$coursefullname} ({$courseshortname})";
|
||||
|
||||
$continueurl = new moodle_url('/course/delete.php', array('id' => $course->id, 'delete' => md5($course->timemodified)));
|
||||
$continuebutton = new single_button($continueurl, get_string('delete'), 'post');
|
||||
$continuebutton = new single_button(
|
||||
$continueurl,
|
||||
get_string('delete'), 'post', false, ['data-action' => 'delete']
|
||||
);
|
||||
echo $OUTPUT->confirm($message, $continuebutton, $categoryurl);
|
||||
// In the following script, we need to use setTimeout as disabling the
|
||||
// button in the event listener script prevent the click to be taken into account.
|
||||
$jsscript = <<<EOF
|
||||
const button = document.querySelector('button[data-action="delete"]');
|
||||
if (button) {
|
||||
button.addEventListener('click', () => {
|
||||
setTimeout(() => {
|
||||
button.disabled = true;
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
EOF;
|
||||
$PAGE->requires->js_amd_inline($jsscript);
|
||||
|
||||
} else {
|
||||
// Async backup is pending, don't let user delete course.
|
||||
echo $OUTPUT->notification(get_string('pendingasyncerror', 'backup'), 'error');
|
||||
|
||||
@@ -495,6 +495,8 @@ class dndupload_ajax_processor {
|
||||
throw new moodle_exception('errornouploadrepo', 'moodle');
|
||||
}
|
||||
$repo = reset($repo); // Get the first (and only) upload repo.
|
||||
// Pre-emptively purge the navigation cache so the upload repo can close the session.
|
||||
navigation_cache::destroy_volatile_caches();
|
||||
$details = $repo->process_upload(null, $maxbytes, $types, '/', $draftitemid);
|
||||
if (empty($this->displayname)) {
|
||||
$this->displayname = $this->display_name_from_file($details['file']);
|
||||
|
||||
+2
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -23,6 +23,7 @@
|
||||
*/
|
||||
|
||||
import {BaseComponent} from 'core/reactive';
|
||||
import {debounce} from 'core/utils';
|
||||
import {getCurrentCourseEditor} from 'core_courseformat/courseeditor';
|
||||
import inplaceeditable from 'core/inplace_editable';
|
||||
import Section from 'core_courseformat/local/content/section';
|
||||
@@ -33,6 +34,8 @@ import DispatchActions from 'core_courseformat/local/content/actions';
|
||||
import * as CourseEvents from 'core_course/events';
|
||||
// The jQuery module is only used for interacting with Boostrap 4. It can we removed when MDL-71979 is integrated.
|
||||
import jQuery from 'jquery';
|
||||
import Pending from 'core/pending';
|
||||
import log from 'core/log';
|
||||
|
||||
export default class Component extends BaseComponent {
|
||||
|
||||
@@ -75,6 +78,7 @@ export default class Component extends BaseComponent {
|
||||
this.cms = {};
|
||||
// The page section return.
|
||||
this.sectionReturn = descriptor.sectionReturn ?? 0;
|
||||
this.debouncedReloads = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,6 +234,8 @@ export default class Component extends BaseComponent {
|
||||
{watch: `transaction:start`, handler: this._startProcessing},
|
||||
{watch: `course.sectionlist:updated`, handler: this._refreshCourseSectionlist},
|
||||
{watch: `section.cmlist:updated`, handler: this._refreshSectionCmlist},
|
||||
// Section visibility.
|
||||
{watch: `section.visible:updated`, handler: this._reloadSection},
|
||||
// Reindex sections and cms.
|
||||
{watch: `state:updated`, handler: this._indexContents},
|
||||
// State changes thaty require to reload course modules.
|
||||
@@ -506,14 +512,65 @@ export default class Component extends BaseComponent {
|
||||
* @param {object} param0.element the state object
|
||||
*/
|
||||
_reloadCm({element}) {
|
||||
const cmitem = this.getElement(this.selectors.CM, element.id);
|
||||
if (cmitem) {
|
||||
const promise = courseActions.refreshModule(cmitem, element.id);
|
||||
if (!this.getElement(this.selectors.CM, element.id)) {
|
||||
return;
|
||||
}
|
||||
const debouncedReload = this._getDebouncedReloadCm(element.id);
|
||||
debouncedReload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate or get a reload CM debounced function.
|
||||
* @param {Number} cmId
|
||||
* @returns {Function} the debounced reload function
|
||||
*/
|
||||
_getDebouncedReloadCm(cmId) {
|
||||
const pendingKey = `courseformat/content:reloadCm_${cmId}`;
|
||||
let debouncedReload = this.debouncedReloads.get(pendingKey);
|
||||
if (debouncedReload) {
|
||||
return debouncedReload;
|
||||
}
|
||||
const reload = () => {
|
||||
const pendingReload = new Pending(pendingKey);
|
||||
this.debouncedReloads.delete(pendingKey);
|
||||
const cmitem = this.getElement(this.selectors.CM, cmId);
|
||||
if (!cmitem) {
|
||||
return pendingReload.resolve();
|
||||
}
|
||||
const promise = courseActions.refreshModule(cmitem, cmId);
|
||||
promise.then(() => {
|
||||
this._indexContents();
|
||||
return;
|
||||
}).catch();
|
||||
return true;
|
||||
}).catch((error) => {
|
||||
log.debug(error);
|
||||
}).finally(() => {
|
||||
pendingReload.resolve();
|
||||
});
|
||||
return pendingReload;
|
||||
};
|
||||
debouncedReload = debounce(
|
||||
reload,
|
||||
200,
|
||||
{
|
||||
cancel: true, pending: true
|
||||
}
|
||||
);
|
||||
this.debouncedReloads.set(pendingKey, debouncedReload);
|
||||
return debouncedReload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the active reload CM debounced function, if any.
|
||||
* @param {Number} cmId
|
||||
*/
|
||||
_cancelDebouncedReloadCm(cmId) {
|
||||
const pendingKey = `courseformat/content:reloadCm_${cmId}`;
|
||||
const debouncedReload = this.debouncedReloads.get(pendingKey);
|
||||
if (!debouncedReload) {
|
||||
return;
|
||||
}
|
||||
debouncedReload.cancel();
|
||||
this.debouncedReloads.delete(pendingKey);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -526,13 +583,22 @@ export default class Component extends BaseComponent {
|
||||
* @param {object} param0.element the state object
|
||||
*/
|
||||
_reloadSection({element}) {
|
||||
const pendingReload = new Pending(`courseformat/content:reloadSection_${element.id}`);
|
||||
const sectionitem = this.getElement(this.selectors.SECTION, element.id);
|
||||
if (sectionitem) {
|
||||
// Cancel any pending reload because the section will reload cms too.
|
||||
for (const cmId of element.cmlist) {
|
||||
this._cancelDebouncedReloadCm(cmId);
|
||||
}
|
||||
const promise = courseActions.refreshSection(sectionitem, element.id);
|
||||
promise.then(() => {
|
||||
this._indexContents();
|
||||
return;
|
||||
}).catch();
|
||||
return true;
|
||||
}).catch((error) => {
|
||||
log.debug(error);
|
||||
}).finally(() => {
|
||||
pendingReload.resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,15 @@
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ajax from 'core/ajax';
|
||||
import {get_string as getString} from "core/str";
|
||||
import SRLogger from "core/local/reactive/srlogger";
|
||||
|
||||
/**
|
||||
* Flag to determine whether the screen reader-only logger has already been set, so we only need to set it once.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
let isLoggerSet = false;
|
||||
|
||||
/**
|
||||
* Default mutation manager
|
||||
@@ -65,6 +74,11 @@ export default class {
|
||||
* @param {number} targetCmId optional target cm id (for moving actions)
|
||||
*/
|
||||
async _sectionBasicAction(stateManager, action, sectionIds, targetSectionId, targetCmId) {
|
||||
const logEntry = this._getLoggerEntry(stateManager, action, sectionIds, {
|
||||
targetSectionId,
|
||||
targetCmId,
|
||||
itemType: 'section',
|
||||
});
|
||||
const course = stateManager.get('course');
|
||||
this.sectionLock(stateManager, sectionIds, true);
|
||||
const updates = await this._callEditWebservice(
|
||||
@@ -76,6 +90,7 @@ export default class {
|
||||
);
|
||||
stateManager.processUpdates(updates);
|
||||
this.sectionLock(stateManager, sectionIds, false);
|
||||
stateManager.addLoggerEntry(await logEntry);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,6 +102,11 @@ export default class {
|
||||
* @param {number} targetCmId optional target cm id (for moving actions)
|
||||
*/
|
||||
async _cmBasicAction(stateManager, action, cmIds, targetSectionId, targetCmId) {
|
||||
const logEntry = this._getLoggerEntry(stateManager, action, cmIds, {
|
||||
targetSectionId,
|
||||
targetCmId,
|
||||
itemType: 'cm',
|
||||
});
|
||||
const course = stateManager.get('course');
|
||||
this.cmLock(stateManager, cmIds, true);
|
||||
const updates = await this._callEditWebservice(
|
||||
@@ -98,6 +118,56 @@ export default class {
|
||||
);
|
||||
stateManager.processUpdates(updates);
|
||||
this.cmLock(stateManager, cmIds, false);
|
||||
stateManager.addLoggerEntry(await logEntry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get log entry for the current action.
|
||||
* @param {StateManager} stateManager the current state manager
|
||||
* @param {string} action the action name
|
||||
* @param {int[]|null} itemIds the element ids
|
||||
* @param {Object|undefined} data extra params for the log entry
|
||||
* @param {string|undefined} data.itemType the element type (will be taken from action if none)
|
||||
* @param {int|null|undefined} data.targetSectionId the target section id
|
||||
* @param {int|null|undefined} data.targetCmId the target cm id
|
||||
* @param {String|null|undefined} data.component optional component (for format plugins)
|
||||
* @return {Object} the log entry
|
||||
*/
|
||||
async _getLoggerEntry(stateManager, action, itemIds, data = {}) {
|
||||
if (!isLoggerSet) {
|
||||
// In case the logger has not been set from init(), ensure we set the logger.
|
||||
stateManager.setLogger(new SRLogger());
|
||||
isLoggerSet = true;
|
||||
}
|
||||
const feedbackParams = {
|
||||
action,
|
||||
itemType: data.itemType ?? action.split('_')[0],
|
||||
};
|
||||
let batch = '';
|
||||
if (itemIds.length > 1) {
|
||||
feedbackParams.count = itemIds.length;
|
||||
batch = '_batch';
|
||||
} else if (itemIds.length === 1) {
|
||||
const itemInfo = stateManager.get(feedbackParams.itemType, itemIds[0]);
|
||||
feedbackParams.name = itemInfo.title ?? itemInfo.name;
|
||||
// Apply shortener for modules like label.
|
||||
}
|
||||
if (data.targetSectionId) {
|
||||
feedbackParams.targetSectionName = stateManager.get('section', data.targetSectionId).title;
|
||||
}
|
||||
if (data.targetCmId) {
|
||||
feedbackParams.targetCmName = stateManager.get('cm', data.targetCmId).name;
|
||||
}
|
||||
|
||||
const message = await getString(
|
||||
`${action.toLowerCase()}_feedback${batch}`,
|
||||
data.component ?? 'core_courseformat',
|
||||
feedbackParams
|
||||
);
|
||||
|
||||
return {
|
||||
feedbackMessage: message,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,10 +178,13 @@ export default class {
|
||||
* @param {StateManager} stateManager the state manager
|
||||
*/
|
||||
init(stateManager) {
|
||||
// Add a method to prepare the fields when some update is comming from the server.
|
||||
// Add a method to prepare the fields when some update is coming from the server.
|
||||
stateManager.addUpdateTypes({
|
||||
prepareFields: this._prepareFields,
|
||||
});
|
||||
// Use the screen reader-only logger (SRLogger) to handle the feedback messages from the mutations.
|
||||
stateManager.setLogger(new SRLogger());
|
||||
isLoggerSet = true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -416,8 +416,13 @@ class stateactions {
|
||||
$allowstealth = !empty($CFG->allowstealth) && $format->allow_stealth_module_visibility($cm, $section);
|
||||
$coursevisible = ($allowstealth) ? 0 : 1;
|
||||
}
|
||||
set_coursemodule_visible($cm->id, $visible, $coursevisible);
|
||||
set_coursemodule_visible($cm->id, $visible, $coursevisible, false);
|
||||
course_module_updated::create_from_cm($cm, $modcontext)->trigger();
|
||||
}
|
||||
course_modinfo::purge_course_modules_cache($course->id, $ids);
|
||||
rebuild_course_cache($course->id, false, true);
|
||||
|
||||
foreach ($cms as $cm) {
|
||||
$updates->add_cm_put($cm->id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,6 +409,7 @@ class format_weeks extends core_courseformat\base {
|
||||
|
||||
// Create a DateTime object for the start date.
|
||||
$startdateobj = new DateTime("@$startdate");
|
||||
$startdateobj->setTimezone(core_date::get_user_timezone_object());
|
||||
|
||||
// Calculate the interval for one week.
|
||||
$oneweekinterval = new DateInterval('P7D');
|
||||
|
||||
+15
-6
@@ -709,12 +709,17 @@ function set_downloadcontent(int $id, bool $downloadcontent): bool {
|
||||
* has been moved to {@link set_section_visible()} which was the only place from which
|
||||
* the parameter was used.
|
||||
*
|
||||
* If $rebuildcache is set to false, the calling code is responsible for ensuring the cache is purged
|
||||
* and rebuilt as appropriate. Consider using this if set_coursemodule_visible is called multiple times
|
||||
* (e.g. in a loop).
|
||||
*
|
||||
* @param int $id of the module
|
||||
* @param int $visible state of the module
|
||||
* @param int $visibleoncoursepage state of the module on the course page
|
||||
* @param bool $rebuildcache If true (default), perform a partial cache purge and rebuild.
|
||||
* @return bool false when the module was not found, true otherwise
|
||||
*/
|
||||
function set_coursemodule_visible($id, $visible, $visibleoncoursepage = 1) {
|
||||
function set_coursemodule_visible($id, $visible, $visibleoncoursepage = 1, bool $rebuildcache = true) {
|
||||
global $DB, $CFG;
|
||||
require_once($CFG->libdir.'/gradelib.php');
|
||||
require_once($CFG->dirroot.'/calendar/lib.php');
|
||||
@@ -771,8 +776,10 @@ function set_coursemodule_visible($id, $visible, $visibleoncoursepage = 1) {
|
||||
}
|
||||
}
|
||||
|
||||
\course_modinfo::purge_course_module_cache($cm->course, $cm->id);
|
||||
rebuild_course_cache($cm->course, false, true);
|
||||
if ($rebuildcache) {
|
||||
\course_modinfo::purge_course_module_cache($cm->course, $cm->id);
|
||||
rebuild_course_cache($cm->course, false, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1457,20 +1464,22 @@ function course_update_section($course, $section, $data) {
|
||||
// If section visibility was changed, hide the modules in this section too.
|
||||
if ($changevisibility && !empty($section->sequence)) {
|
||||
$modules = explode(',', $section->sequence);
|
||||
$cmids = [];
|
||||
foreach ($modules as $moduleid) {
|
||||
if ($cm = get_coursemodule_from_id(null, $moduleid, $courseid)) {
|
||||
$cmids[] = $cm->id;
|
||||
if ($data['visible']) {
|
||||
// As we unhide the section, we use the previously saved visibility stored in visibleold.
|
||||
set_coursemodule_visible($moduleid, $cm->visibleold, $cm->visibleoncoursepage);
|
||||
set_coursemodule_visible($moduleid, $cm->visibleold, $cm->visibleoncoursepage, false);
|
||||
} else {
|
||||
// We hide the section, so we hide the module but we store the original state in visibleold.
|
||||
set_coursemodule_visible($moduleid, 0, $cm->visibleoncoursepage);
|
||||
set_coursemodule_visible($moduleid, 0, $cm->visibleoncoursepage, false);
|
||||
$DB->set_field('course_modules', 'visibleold', $cm->visible, ['id' => $moduleid]);
|
||||
\course_modinfo::purge_course_module_cache($cm->course, $cm->id);
|
||||
}
|
||||
\core\event\course_module_updated::create_from_cm($cm)->trigger();
|
||||
}
|
||||
}
|
||||
\course_modinfo::purge_course_modules_cache($courseid, $cmids);
|
||||
rebuild_course_cache($courseid, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
+8
-2
@@ -110,8 +110,14 @@ if (empty($pending)) {
|
||||
echo $OUTPUT->heading(get_string('nopendingcourses'));
|
||||
} else {
|
||||
echo $OUTPUT->heading(get_string('coursespending'));
|
||||
$role = $DB->get_record('role', array('id' => $CFG->creatornewroleid), '*', MUST_EXIST);
|
||||
echo $OUTPUT->notification(get_string('courserequestwarning', 'core', role_get_name($role)), 'notifyproblem');
|
||||
|
||||
$role = $DB->get_record('role', ['id' => $CFG->creatornewroleid]);
|
||||
if ($role) {
|
||||
echo $OUTPUT->notification(get_string('courserequestwarning', 'core', role_get_name($role)), 'notifyproblem');
|
||||
} else {
|
||||
$userpoliciesurl = new moodle_url('/admin/settings.php', ['section' => 'userpolicies']);
|
||||
echo $OUTPUT->notification(get_string('courserequestroleerror', 'core', (string) $userpoliciesurl), 'notifyerror');
|
||||
}
|
||||
|
||||
/// Build a table of all the requests.
|
||||
$table = new html_table();
|
||||
|
||||
@@ -70,14 +70,14 @@
|
||||
"options": [
|
||||
{
|
||||
"url": "\/course\/index.php\/?categoryid=281",
|
||||
"string": "Action 1"
|
||||
"string": "Action 1",
|
||||
"attributes": [
|
||||
{
|
||||
"name": "data-id",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"name": "aria-role",
|
||||
"name": "data-action",
|
||||
"value": "test"
|
||||
}
|
||||
]
|
||||
@@ -105,9 +105,9 @@
|
||||
{{#str}}moremenu{{/str}}
|
||||
</button>
|
||||
|
||||
<div id="moreactionsmenu" class="dropdown-menu" aria-labelledby="dropdown-actions" role="menu">
|
||||
<div id="moreactionsmenu" class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdown-actions" role="menu">
|
||||
{{#options}}
|
||||
<a role="menuitem" class="dropdown-item" {{#attributes}}{{name}}="{{value}}"{{/attributes}} href="{{url}}" tabindex="-1">{{string}}</a>
|
||||
<a role="menuitem" class="dropdown-item" {{#attributes}}{{name}}="{{value}}" {{/attributes}} href="{{url}}" tabindex="-1">{{string}}</a>
|
||||
{{/options}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,9 +11,8 @@ Feature: Add activities to courses
|
||||
|
||||
@javascript
|
||||
Scenario: Add an activity to a course
|
||||
Given I am on the "Course 1" Course page logged in as admin
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
When I add a "Database" to section "3" and I fill the form with:
|
||||
Given I log in as "admin"
|
||||
When I add a data activity to course "Course 1" section "3" and I fill the form with:
|
||||
| Name | Test name |
|
||||
| Description | Test database description |
|
||||
| ID number | TESTNAME |
|
||||
@@ -32,9 +31,8 @@ Feature: Add activities to courses
|
||||
|
||||
@javascript
|
||||
Scenario: Add an activity supplying only the name
|
||||
Given I am on the "Course 1" Course page logged in as admin
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
When I add a "Database" to section "3" and I fill the form with:
|
||||
Given I log in as "admin"
|
||||
When I add a data activity to course "Course 1" section "3" and I fill the form with:
|
||||
| Name | Test name |
|
||||
Then I should see "Test name"
|
||||
|
||||
@@ -42,9 +40,8 @@ Feature: Add activities to courses
|
||||
Scenario: Set activity description to required then add an activity supplying only the name
|
||||
Given the following config values are set as admin:
|
||||
| requiremodintro | 1 |
|
||||
And I am on the "Course 1" Course page logged in as admin
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "Database" to section "3" and I fill the form with:
|
||||
And I log in as "admin"
|
||||
And I add a data activity to course "Course 1" section "3" and I fill the form with:
|
||||
| Name | Test name |
|
||||
Then I should see "Required"
|
||||
|
||||
@@ -53,9 +50,8 @@ Feature: Add activities to courses
|
||||
Given the following "user preferences" exist:
|
||||
| user | preference | value |
|
||||
| admin | htmleditor | textarea |
|
||||
And I am logged in as admin
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
When I add a "Database" to section "3"
|
||||
And I log in as "admin"
|
||||
When I add a data activity to course "Course 1" section "3"
|
||||
Then the field "Description format" matches value "0"
|
||||
|
||||
@javascript
|
||||
|
||||
@@ -202,7 +202,7 @@ class behat_course extends behat_base {
|
||||
if ($section) {
|
||||
// Section 1 represents the contents on the frontpage.
|
||||
$sectionxpath = "//body[@id='page-site-index']" .
|
||||
"/descendant::div[contains(concat(' ',normalize-space(@class),' '),' sitetopic ')]";
|
||||
"/descendant::div[contains(concat(' ',normalize-space(@class),' '),' sitetopic ')]";
|
||||
} else {
|
||||
// Section 0 represents "Site main menu" block.
|
||||
$sectionxpath = "//*[contains(concat(' ',normalize-space(@class),' '),' block_site_main_menu ')]";
|
||||
@@ -224,10 +224,116 @@ class behat_course extends behat_base {
|
||||
// Clicks the selected activity if it exists.
|
||||
$activityliteral = behat_context_helper::escape(ucfirst($activity));
|
||||
$activityxpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' modchooser ')]" .
|
||||
"/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' optioninfo ')]" .
|
||||
"/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' optionname ')]" .
|
||||
"[normalize-space(.)=$activityliteral]" .
|
||||
"/parent::a";
|
||||
"/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' optioninfo ')]" .
|
||||
"/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' optionname ')]" .
|
||||
"[normalize-space(.)=$activityliteral]" .
|
||||
"/parent::a";
|
||||
|
||||
$this->execute('behat_general::i_click_on', [$activityxpath, 'xpath']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the selected activity/resource filling the form data with the specified field/value pairs.
|
||||
*
|
||||
* Sections 0 and 1 are also allowed on frontpage.
|
||||
*
|
||||
* @Given I add a :activity activity to course :coursefullname section :sectionnum and I fill the form with:
|
||||
* @Given I add an :activity activity to course :coursefullname section :sectionnum and I fill the form with:
|
||||
* @param string $activity The activity name
|
||||
* @param string $coursefullname The course full name of the course.
|
||||
* @param int $section The section number
|
||||
* @param TableNode $data The activity field/value data
|
||||
*/
|
||||
public function i_add_to_course_section_and_i_fill_the_form_with($activity, $coursefullname, $section, TableNode $data) {
|
||||
|
||||
// Add activity to section.
|
||||
$this->execute(
|
||||
"behat_course::i_add_to_course_section",
|
||||
[$this->escape($activity), $this->escape($coursefullname), $this->escape($section)]
|
||||
);
|
||||
|
||||
// Wait to be redirected.
|
||||
$this->execute('behat_general::wait_until_the_page_is_ready');
|
||||
|
||||
// Set form fields.
|
||||
$this->execute("behat_forms::i_set_the_following_fields_to_these_values", $data);
|
||||
|
||||
// Save course settings.
|
||||
$this->execute("behat_forms::press_button", get_string('savechangesandreturntocourse'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a add activity form page.
|
||||
*
|
||||
* @Given I add a :activity activity to course :coursefullname section :sectionnum
|
||||
* @Given I add an :activity activity to course :coursefullname section :sectionnum
|
||||
* @throws coding_exception
|
||||
* @param string $activity The activity name.
|
||||
* @param string $coursefullname The course full name of the course.
|
||||
* @param string $sectionnum The section number.
|
||||
*/
|
||||
public function i_add_to_course_section(string $activity, string $coursefullname, string $sectionnum): void {
|
||||
$addurl = new moodle_url('/course/modedit.php', [
|
||||
'add' => $activity,
|
||||
'course' => $this->get_course_id($coursefullname),
|
||||
'section' => intval($sectionnum),
|
||||
]);
|
||||
$this->execute('behat_general::i_visit', [$addurl]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the activity chooser and opens the activity/resource link form page.
|
||||
*
|
||||
* Sections 0 and 1 are also allowed on frontpage.
|
||||
*
|
||||
* This step require javascript enabled and it is used mainly to click activities or resources by name,
|
||||
* not by plugin name. Use the standard behat_course::i_add_to_course_section step instead unless the
|
||||
* plugin create extra entries into the activity chooser (like LTI).
|
||||
*
|
||||
* @Given I add a :activityname to section :sectionnum using the activity chooser
|
||||
* @Given I add an :activityname to section :sectionnum using the activity chooser
|
||||
* @throws ElementNotFoundException Thrown by behat_base::find
|
||||
* @param string $activityname
|
||||
* @param int $sectionnum
|
||||
*/
|
||||
public function i_add_to_section_using_the_activity_chooser($activityname, $sectionnum) {
|
||||
|
||||
$this->require_javascript('Please use the \'the following "activity" exists:\' data generator instead.');
|
||||
|
||||
if ($this->getSession()->getPage()->find('css', 'body#page-site-index') && (int) $sectionnum <= 1) {
|
||||
// We are on the frontpage.
|
||||
if ($sectionnum) {
|
||||
// Section 1 represents the contents on the frontpage.
|
||||
$sectionxpath = "//body[@id='page-site-index']" .
|
||||
"/descendant::div[contains(concat(' ',normalize-space(@class),' '),' sitetopic ')]";
|
||||
} else {
|
||||
// Section 0 represents "Site main menu" block.
|
||||
$sectionxpath = "//*[contains(concat(' ',normalize-space(@class),' '),' block_site_main_menu ')]";
|
||||
}
|
||||
} else {
|
||||
// We are inside the course.
|
||||
$sectionxpath = "//li[@id='section-" . $sectionnum . "']";
|
||||
}
|
||||
|
||||
// Clicks add activity or resource section link.
|
||||
$sectionnode = $this->find('xpath', $sectionxpath);
|
||||
$this->execute(
|
||||
'behat_general::i_click_on_in_the',
|
||||
[
|
||||
"//button[@data-action='open-chooser' and not(@data-beforemod)]",
|
||||
'xpath',
|
||||
$sectionnode,
|
||||
'NodeElement',
|
||||
]
|
||||
);
|
||||
|
||||
// Clicks the selected activity if it exists.
|
||||
$activityliteral = behat_context_helper::escape(ucfirst($activityname));
|
||||
$activityxpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' modchooser ')]" .
|
||||
"/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' optioninfo ')]" .
|
||||
"/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' optionname ')]" .
|
||||
"[normalize-space(.)=$activityliteral]" .
|
||||
"/parent::a";
|
||||
|
||||
$this->execute('behat_general::i_click_on', [$activityxpath, 'xpath']);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ Feature: Users can request and approve courses
|
||||
| Course short name | Mynewcourse |
|
||||
| Supporting information | pretty please |
|
||||
And I press "Request a course"
|
||||
And I should see "Your course request has been saved successfully."
|
||||
And I should see "Course request submitted."
|
||||
And I press "Continue"
|
||||
And I am on course index
|
||||
And I should not see "My new course"
|
||||
|
||||
@@ -1333,6 +1333,53 @@ class courselib_test extends advanced_testcase {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test rebuildcache = false behaviour.
|
||||
*
|
||||
* When we pass rebuildcache = false to set_coursemodule_visible, the corusemodinfo cache will still contain
|
||||
* the original visibility until we trigger a rebuild.
|
||||
*
|
||||
* @return void
|
||||
* @covers ::set_coursemodule_visible
|
||||
*/
|
||||
public function test_module_visibility_no_rebuild(): void {
|
||||
$this->setAdminUser();
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
// Create course and modules.
|
||||
$course = $this->getDataGenerator()->create_course(['numsections' => 5]);
|
||||
$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
|
||||
$assign = $this->getDataGenerator()->create_module('assign', ['duedate' => time(), 'course' => $course->id]);
|
||||
$modules = compact('forum', 'assign');
|
||||
|
||||
// Hiding the modules.
|
||||
foreach ($modules as $mod) {
|
||||
set_coursemodule_visible($mod->cmid, 0, 1, false);
|
||||
// The modinfo cache still has the original visibility until we manually trigger a rebuild.
|
||||
$cm = get_fast_modinfo($mod->course)->get_cm($mod->cmid);
|
||||
$this->assertEquals(1, $cm->visible);
|
||||
}
|
||||
|
||||
rebuild_course_cache($course->id);
|
||||
|
||||
foreach ($modules as $mod) {
|
||||
$this->check_module_visibility($mod, 0, 0);
|
||||
}
|
||||
|
||||
// Showing the modules.
|
||||
foreach ($modules as $mod) {
|
||||
set_coursemodule_visible($mod->cmid, 1, 1, false);
|
||||
$cm = get_fast_modinfo($mod->course)->get_cm($mod->cmid);
|
||||
$this->assertEquals(0, $cm->visible);
|
||||
}
|
||||
|
||||
rebuild_course_cache($course->id);
|
||||
|
||||
foreach ($modules as $mod) {
|
||||
$this->check_module_visibility($mod, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_section_visibility_events() {
|
||||
$this->setAdminUser();
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
This files describes API changes in /course/*,
|
||||
information provided here is intended especially for developers.
|
||||
|
||||
=== 4.1.7 ===
|
||||
* New behat steps backported to stable versions:
|
||||
- I add a :activityname to section :sectionnum using the activity chooser
|
||||
- I add a :activitypluginname activity to course :coursefullname section :sectionnum
|
||||
- I add a :activitypluginname activity to course :coursefullname section :sectionnum and I fill the form with:
|
||||
* set_coursemodule_visible() has a new $rebuildcache parameter. If this is being called multiple times in the same request,
|
||||
consider passing `false` for this parameter and rebuilding the cache once after all the course modules have been updated.
|
||||
See course_update_section() for an example.
|
||||
|
||||
=== 4.1 ===
|
||||
* The function course_modchooser() has been finally deprecated and can not be used anymore. Please use
|
||||
course_activitychooser() instead.
|
||||
|
||||
@@ -238,7 +238,13 @@ abstract class data_controller {
|
||||
*/
|
||||
protected function is_unique($value) : bool {
|
||||
global $DB;
|
||||
|
||||
// Ensure the "value" datafield can be safely compared across all databases.
|
||||
$datafield = $this->datafield();
|
||||
if ($datafield === 'value') {
|
||||
$datafield = $DB->sql_cast_to_char($datafield);
|
||||
}
|
||||
|
||||
$where = "fieldid = ? AND {$datafield} = ?";
|
||||
$params = [$this->get_field()->get('id'), $value];
|
||||
if ($this->get('id')) {
|
||||
|
||||
@@ -136,6 +136,29 @@ class data_controller extends \core_customfield\data_controller {
|
||||
$instance->{$this->get_form_element_name()} = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the value is empty, overriding the base method to ensure it's the "text" element of our value being compared
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_empty($value): bool {
|
||||
if (is_array($value)) {
|
||||
$value = $value['text'];
|
||||
}
|
||||
return html_is_blank($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the value is unique, overriding the base method to ensure it's the "text" element of our value being compared
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_unique($value): bool {
|
||||
return parent::is_unique($value['text']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete data
|
||||
*
|
||||
@@ -166,9 +189,6 @@ class data_controller extends \core_customfield\data_controller {
|
||||
require_once($CFG->libdir . '/filelib.php');
|
||||
|
||||
$value = $this->get_value();
|
||||
if ($this->is_empty($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($dataid = $this->get('id')) {
|
||||
$context = $this->get_context();
|
||||
|
||||
+20
-9
@@ -619,6 +619,7 @@ class core_enrol_external extends external_api {
|
||||
'searchanywhere' => new external_value(PARAM_BOOL, 'find a match anywhere, or only at the beginning'),
|
||||
'page' => new external_value(PARAM_INT, 'Page number'),
|
||||
'perpage' => new external_value(PARAM_INT, 'Number per page'),
|
||||
'contextid' => new external_value(PARAM_INT, 'Context ID', VALUE_DEFAULT, null),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -631,11 +632,12 @@ class core_enrol_external extends external_api {
|
||||
* @param bool $searchanywhere Match anywhere in the string
|
||||
* @param int $page Page number
|
||||
* @param int $perpage Max per page
|
||||
* @param ?int $contextid Context ID we are in - we might use search on activity level and its group mode can be different from course group mode.
|
||||
* @return array An array of users
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public static function search_users(int $courseid, string $search, bool $searchanywhere, int $page, int $perpage): array {
|
||||
global $PAGE, $DB, $CFG;
|
||||
public static function search_users(int $courseid, string $search, bool $searchanywhere, int $page, int $perpage, ?int $contextid = null): array {
|
||||
global $PAGE, $CFG;
|
||||
|
||||
require_once($CFG->dirroot.'/enrol/locallib.php');
|
||||
require_once($CFG->dirroot.'/user/lib.php');
|
||||
@@ -647,10 +649,15 @@ class core_enrol_external extends external_api {
|
||||
'search' => $search,
|
||||
'searchanywhere' => $searchanywhere,
|
||||
'page' => $page,
|
||||
'perpage' => $perpage
|
||||
]
|
||||
'perpage' => $perpage,
|
||||
'contextid' => $contextid,
|
||||
],
|
||||
);
|
||||
$context = context_course::instance($params['courseid']);
|
||||
if (isset($contextid)) {
|
||||
$context = context::instance_by_id($params['contextid']);
|
||||
} else {
|
||||
$context = context_course::instance($params['courseid']);
|
||||
}
|
||||
try {
|
||||
self::validate_context($context);
|
||||
} catch (Exception $e) {
|
||||
@@ -664,10 +671,14 @@ class core_enrol_external extends external_api {
|
||||
$course = get_course($params['courseid']);
|
||||
$manager = new course_enrolment_manager($PAGE, $course);
|
||||
|
||||
$users = $manager->search_users($params['search'],
|
||||
$params['searchanywhere'],
|
||||
$params['page'],
|
||||
$params['perpage']);
|
||||
$users = $manager->search_users(
|
||||
$params['search'],
|
||||
$params['searchanywhere'],
|
||||
$params['page'],
|
||||
$params['perpage'],
|
||||
false,
|
||||
$params['contextid']
|
||||
);
|
||||
|
||||
$results = [];
|
||||
// Add also extra user fields.
|
||||
|
||||
+18
-4
@@ -563,26 +563,40 @@ class course_enrolment_manager {
|
||||
* @param int $page Starting at 0.
|
||||
* @param int $perpage Number of users returned per page.
|
||||
* @param bool $returnexactcount Return the exact total users using count_record or not.
|
||||
* @param ?int $contextid Context ID we are in - we might use search on activity level and its group mode can be different from course group mode.
|
||||
* @return array with two or three elements:
|
||||
* int totalusers Number users matching the search. (This element only exist if $returnexactcount was set to true)
|
||||
* array users List of user objects returned by the query.
|
||||
* boolean moreusers True if there are still more users, otherwise is False.
|
||||
*/
|
||||
public function search_users(string $search = '', bool $searchanywhere = false, int $page = 0, int $perpage = 25,
|
||||
bool $returnexactcount = false) {
|
||||
bool $returnexactcount = false, ?int $contextid = null) {
|
||||
global $USER;
|
||||
|
||||
[$ufields, $joins, $params, $wherecondition] = $this->get_basic_search_conditions($search, $searchanywhere);
|
||||
|
||||
$groupmode = groups_get_course_groupmode($this->course);
|
||||
if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $this->context)) {
|
||||
if (isset($contextid)) {
|
||||
// If contextid is set, we need to determine the group mode that should be used (module or course).
|
||||
[$context, $course, $cm] = get_context_info_array($contextid);
|
||||
// If cm instance is returned, then use the group mode from the module, otherwise get the course group mode.
|
||||
$groupmode = $cm ? groups_get_activity_groupmode($cm, $course) : groups_get_course_groupmode($this->course);
|
||||
} else {
|
||||
// Otherwise, default to the group mode of the course.
|
||||
$context = $this->context;
|
||||
$groupmode = groups_get_course_groupmode($this->course);
|
||||
}
|
||||
|
||||
if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $context)) {
|
||||
$groups = groups_get_all_groups($this->course->id, $USER->id, 0, 'g.id');
|
||||
$groupids = array_column($groups, 'id');
|
||||
if (!$groupids) {
|
||||
return ['totalusers' => 0, 'users' => [], 'moreusers' => false];
|
||||
}
|
||||
} else {
|
||||
$groupids = [];
|
||||
}
|
||||
|
||||
[$enrolledsql, $enrolledparams] = get_enrolled_sql($this->context, '', $groupids);
|
||||
[$enrolledsql, $enrolledparams] = get_enrolled_sql($context, '', $groupids);
|
||||
|
||||
$fields = 'SELECT ' . $ufields;
|
||||
$countfields = 'SELECT COUNT(u.id)';
|
||||
|
||||
@@ -173,8 +173,8 @@ class enrol_meta_handler {
|
||||
$ue->userid = $userid;
|
||||
$ue->enrolid = $instance->id;
|
||||
$ue->status = $parentstatus;
|
||||
if ($instance->customint2) {
|
||||
groups_add_member($instance->customint2, $userid, 'enrol_meta', $instance->id);
|
||||
if ($instance->customint2 && $group = $DB->get_record('groups', ['id' => $instance->customint2])) {
|
||||
groups_add_member($group, $userid, 'enrol_meta', $instance->id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,8 +346,8 @@ function enrol_meta_sync($courseid = NULL, $verbose = false) {
|
||||
$ue->timestart = ($ue->timestart == 9999999999) ? 0 : (int)$ue->timestart;
|
||||
|
||||
$meta->enrol_user($instance, $ue->userid, null, $ue->timestart, $ue->timeend, $ue->status);
|
||||
if ($instance->customint2) {
|
||||
groups_add_member($instance->customint2, $ue->userid, 'enrol_meta', $instance->id);
|
||||
if ($instance->customint2 && $group = $DB->get_record('groups', ['id' => $instance->customint2])) {
|
||||
groups_add_member($group, $ue->userid, 'enrol_meta', $instance->id);
|
||||
}
|
||||
if ($verbose) {
|
||||
mtrace(" enrolling: $ue->userid ==> $instance->courseid");
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
namespace enrol_meta;
|
||||
|
||||
use context_course;
|
||||
use enrol_meta_plugin;
|
||||
|
||||
/**
|
||||
* Meta enrolment sync functional test.
|
||||
*
|
||||
@@ -594,6 +597,40 @@ class plugin_test extends \advanced_testcase {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Test enrolling users in a course, where the customint2 (group) property of the instance points to an invalid group
|
||||
*
|
||||
* @covers \enrol_meta_handler::sync_with_parent_course
|
||||
* @covers ::enrol_meta_sync
|
||||
*/
|
||||
public function test_add_to_group_invalid(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$this->enable_plugin();
|
||||
|
||||
$courseone = $this->getDataGenerator()->create_course();
|
||||
$coursetwo = $this->getDataGenerator()->create_course();
|
||||
|
||||
/** @var enrol_meta_plugin $plugin */
|
||||
$plugin = enrol_get_plugin('meta');
|
||||
$plugin->add_instance($coursetwo, ['customint1' => $courseone->id, 'customint2' => 42]);
|
||||
|
||||
// Ensure the event observer works for invalid groups.
|
||||
$userone = $this->getDataGenerator()->create_and_enrol($courseone);
|
||||
|
||||
// Now disable the plugin, add another enrolment.
|
||||
$this->disable_plugin();
|
||||
$usertwo = $this->getDataGenerator()->create_and_enrol($courseone);
|
||||
|
||||
// Re-enable the plugin, run sync task - should also work for invalid groups.
|
||||
$this->enable_plugin();
|
||||
enrol_meta_sync($coursetwo->id);
|
||||
|
||||
$coursetwocontext = context_course::instance($coursetwo->id);
|
||||
$this->assertTrue(is_enrolled($coursetwocontext, $userone));
|
||||
$this->assertTrue(is_enrolled($coursetwocontext, $usertwo));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test user_enrolment_created event.
|
||||
*/
|
||||
|
||||
@@ -92,7 +92,7 @@ If an enrolment key is specified, any user attempting to enrol in the course wil
|
||||
$string['passwordinvalid'] = 'Incorrect enrolment key, please try again';
|
||||
$string['passwordinvalidhint'] = 'That enrolment key was incorrect, please try again<br />
|
||||
(Here\'s a hint - it starts with \'{$a}\')';
|
||||
$string['passwordmatchesgroupkey'] = 'Enrolment key matches an existing group enrolment key';
|
||||
$string['passwordmatchesgroupkey'] = 'This enrolment key is already used as a group enrolment key.';
|
||||
$string['pluginname'] = 'Self enrolment';
|
||||
$string['pluginname_desc'] = 'The self enrolment plugin allows users to choose which courses they want to participate in. The courses may be protected by an enrolment key. Internally the enrolment is done via the manual enrolment plugin which has to be enabled in the same course.';
|
||||
$string['requirepassword'] = 'Require enrolment key';
|
||||
|
||||
@@ -800,7 +800,7 @@ class self_test extends \advanced_testcase {
|
||||
] + (array) $instance, [], $instance, $context);
|
||||
|
||||
$this->assertArrayHasKey('password', $errors);
|
||||
$this->assertEquals('Enrolment key matches an existing group enrolment key', $errors['password']);
|
||||
$this->assertEquals('This enrolment key is already used as a group enrolment key.', $errors['password']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace core_enrol;
|
||||
|
||||
use context_course;
|
||||
use course_enrolment_manager;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Test course_enrolment_manager parts.
|
||||
@@ -557,11 +558,18 @@ class course_enrolment_manager_test extends \advanced_testcase {
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create the forum.
|
||||
$record = new stdClass();
|
||||
$record->introformat = FORMAT_HTML;
|
||||
$record->course = $this->course->id;
|
||||
$forum = self::getDataGenerator()->create_module('forum', $record, ['groupmode' => SEPARATEGROUPS]);
|
||||
$contextid = $DB->get_field('context', 'id', ['instanceid' => $forum->cmid, 'contextlevel' => CONTEXT_MODULE]);
|
||||
|
||||
$teacher = $this->getDataGenerator()->create_and_enrol($this->course, 'teacher');
|
||||
$this->getDataGenerator()->create_group_member(['groupid' => $this->groups['group1']->id, 'userid' => $teacher->id]);
|
||||
$this->setUser($teacher);
|
||||
|
||||
$users = (new course_enrolment_manager($PAGE, $this->course))->search_users('', false, 0, 25, true);
|
||||
$courseusers = (new course_enrolment_manager($PAGE, $this->course))->search_users('', false, 0, 25, true);
|
||||
$this->assertEqualsCanonicalizing([
|
||||
$teacher->username,
|
||||
$this->users['user0']->username,
|
||||
@@ -570,26 +578,49 @@ class course_enrolment_manager_test extends \advanced_testcase {
|
||||
$this->users['user22']->username,
|
||||
$this->users['userall']->username,
|
||||
$this->users['usertch']->username,
|
||||
], array_column($users['users'], 'username'));
|
||||
$this->assertEquals(7, $users['totalusers']);
|
||||
], array_column($courseusers['users'], 'username'));
|
||||
$this->assertEquals(7, $courseusers['totalusers']);
|
||||
|
||||
// Switch course to separate groups.
|
||||
$this->course->groupmode = SEPARATEGROUPS;
|
||||
update_course($this->course);
|
||||
|
||||
$users = (new course_enrolment_manager($PAGE, $this->course))->search_users('', false, 0, 25, true);
|
||||
$forumusers = (new course_enrolment_manager($PAGE, $this->course))->search_users('', false, 0, 25, true, $contextid);
|
||||
$this->assertEqualsCanonicalizing([
|
||||
$teacher->username,
|
||||
$this->users['user1']->username,
|
||||
$this->users['userall']->username,
|
||||
], array_column($users['users'], 'username'));
|
||||
$this->assertEquals(3, $users['totalusers']);
|
||||
], array_column($forumusers['users'], 'username'));
|
||||
$this->assertEquals(3, $forumusers['totalusers']);
|
||||
|
||||
// Switch course to separate groups and forum to no group.
|
||||
$this->course->groupmode = SEPARATEGROUPS;
|
||||
update_course($this->course);
|
||||
set_coursemodule_groupmode($forum->cmid, NOGROUPS);
|
||||
|
||||
$courseusers = (new course_enrolment_manager($PAGE, $this->course))->search_users('', false, 0, 25, true);
|
||||
$this->assertEqualsCanonicalizing([
|
||||
$teacher->username,
|
||||
$this->users['user1']->username,
|
||||
$this->users['userall']->username,
|
||||
], array_column($courseusers['users'], 'username'));
|
||||
$this->assertEquals(3, $courseusers['totalusers']);
|
||||
|
||||
$forumusers = (new course_enrolment_manager($PAGE, $this->course))->search_users('', false, 0, 25, true, $contextid);
|
||||
$this->assertEqualsCanonicalizing([
|
||||
$teacher->username,
|
||||
$this->users['user0']->username,
|
||||
$this->users['user1']->username,
|
||||
$this->users['user21']->username,
|
||||
$this->users['user22']->username,
|
||||
$this->users['userall']->username,
|
||||
$this->users['usertch']->username,
|
||||
], array_column($forumusers['users'], 'username'));
|
||||
$this->assertEquals(7, $forumusers['totalusers']);
|
||||
|
||||
set_coursemodule_groupmode($forum->cmid, SEPARATEGROUPS);
|
||||
|
||||
// Allow teacher to access all groups.
|
||||
$roleid = $DB->get_field('role', 'id', ['shortname' => 'teacher']);
|
||||
assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $roleid, context_course::instance($this->course->id)->id);
|
||||
|
||||
$users = (new course_enrolment_manager($PAGE, $this->course))->search_users('', false, 0, 25, true);
|
||||
$courseusers = (new course_enrolment_manager($PAGE, $this->course))->search_users('', false, 0, 25, true);
|
||||
$this->assertEqualsCanonicalizing([
|
||||
$teacher->username,
|
||||
$this->users['user0']->username,
|
||||
@@ -598,7 +629,19 @@ class course_enrolment_manager_test extends \advanced_testcase {
|
||||
$this->users['user22']->username,
|
||||
$this->users['userall']->username,
|
||||
$this->users['usertch']->username,
|
||||
], array_column($users['users'], 'username'));
|
||||
$this->assertEquals(7, $users['totalusers']);
|
||||
], array_column($courseusers['users'], 'username'));
|
||||
$this->assertEquals(7, $courseusers['totalusers']);
|
||||
|
||||
$forumusers = (new course_enrolment_manager($PAGE, $this->course))->search_users('', false, 0, 25, true, $contextid);
|
||||
$this->assertEqualsCanonicalizing([
|
||||
$teacher->username,
|
||||
$this->users['user0']->username,
|
||||
$this->users['user1']->username,
|
||||
$this->users['user21']->username,
|
||||
$this->users['user22']->username,
|
||||
$this->users['userall']->username,
|
||||
$this->users['usertch']->username,
|
||||
], array_column($forumusers['users'], 'username'));
|
||||
$this->assertEquals(7, $forumusers['totalusers']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1122,6 +1122,43 @@ class enrollib_test extends advanced_testcase {
|
||||
$this->assertCount(1, enrol_get_course_users($course1->id, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* test_course_users in groups
|
||||
*
|
||||
* @covers \enrol_get_course_users()
|
||||
* @return void
|
||||
*/
|
||||
public function test_course_users_in_groups() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$user3 = $this->getDataGenerator()->create_user();
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
|
||||
$group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]);
|
||||
|
||||
$this->getDataGenerator()->enrol_user($user1->id, $course->id);
|
||||
$this->getDataGenerator()->enrol_user($user2->id, $course->id);
|
||||
$this->getDataGenerator()->enrol_user($user3->id, $course->id);
|
||||
|
||||
$this->getDataGenerator()->create_group_member(['groupid' => $group1->id, 'userid' => $user1->id]);
|
||||
$this->getDataGenerator()->create_group_member(['groupid' => $group2->id, 'userid' => $user1->id]);
|
||||
$this->getDataGenerator()->create_group_member(['groupid' => $group2->id, 'userid' => $user2->id]);
|
||||
|
||||
$this->assertCount(3, enrol_get_course_users($course->id));
|
||||
$this->assertCount(1, enrol_get_course_users($course->id, false, [], [], [$group1->id]));
|
||||
$this->assertCount(2, enrol_get_course_users($course->id, false, [], [], [$group2->id]));
|
||||
|
||||
$instances = enrol_get_instances($course->id, true);
|
||||
$manualinstance = reset($instances);
|
||||
|
||||
$manualplugin = enrol_get_plugin('manual');
|
||||
$manualplugin->update_user_enrol($manualinstance, $user1->id, ENROL_USER_SUSPENDED);
|
||||
$this->assertCount(2, enrol_get_course_users($course->id, false, [], [], [$group2->id]));
|
||||
$this->assertCount(1, enrol_get_course_users($course->id, true, [], [], [$group2->id]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test count of enrolled users
|
||||
*
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace core_enrol;
|
||||
use core_enrol_external;
|
||||
use enrol_user_enrolment_form;
|
||||
use externallib_advanced_testcase;
|
||||
use stdClass;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@@ -1402,6 +1403,71 @@ class externallib_test extends externallib_advanced_testcase {
|
||||
$this->assertCount(0, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for core_enrol_external::search_users() when group mode is active.
|
||||
* @covers ::search_users
|
||||
*/
|
||||
public function test_search_users_groupmode() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$datagen = $this->getDataGenerator();
|
||||
|
||||
$studentroleid = $DB->get_field('role', 'id', ['shortname' => 'student'], MUST_EXIST);
|
||||
$teacherroleid = $DB->get_field('role', 'id', ['shortname' => 'teacher'], MUST_EXIST);
|
||||
|
||||
$course = $datagen->create_course();
|
||||
|
||||
$student1 = $datagen->create_and_enrol($course);
|
||||
$student2 = $datagen->create_and_enrol($course);
|
||||
$student3 = $datagen->create_and_enrol($course);
|
||||
$teacher1 = $datagen->create_and_enrol($course, 'teacher');
|
||||
$teacher2 = $datagen->create_and_enrol($course, 'teacher');
|
||||
$teacher3 = $datagen->create_and_enrol($course, 'teacher');
|
||||
$teacher4 = $datagen->create_and_enrol($course, 'editingteacher');
|
||||
|
||||
// Create 2 groups.
|
||||
$group1 = $datagen->create_group(['courseid' => $course->id]);
|
||||
$group2 = $datagen->create_group(['courseid' => $course->id]);
|
||||
|
||||
// Add the users to the groups.
|
||||
$datagen->create_group_member(['groupid' => $group1->id, 'userid' => $student1->id]);
|
||||
$datagen->create_group_member(['groupid' => $group2->id, 'userid' => $student2->id]);
|
||||
$datagen->create_group_member(['groupid' => $group2->id, 'userid' => $student3->id]);
|
||||
$datagen->create_group_member(['groupid' => $group1->id, 'userid' => $teacher1->id]);
|
||||
$datagen->create_group_member(['groupid' => $group2->id, 'userid' => $teacher1->id]);
|
||||
$datagen->create_group_member(['groupid' => $group1->id, 'userid' => $teacher2->id]);
|
||||
|
||||
// Create the forum.
|
||||
$record = new stdClass();
|
||||
$record->introformat = FORMAT_HTML;
|
||||
$record->course = $course->id;
|
||||
$forum = self::getDataGenerator()->create_module('forum', $record, ['groupmode' => SEPARATEGROUPS]);
|
||||
$contextid = $DB->get_field('context', 'id', ['instanceid' => $forum->cmid, 'contextlevel' => CONTEXT_MODULE]);
|
||||
|
||||
$this->setUser($teacher1);
|
||||
$result = core_enrol_external::search_users($course->id, 'user', true, 0, 30, $contextid);
|
||||
$this->assertCount(5, $result);
|
||||
|
||||
$this->setUser($teacher2);
|
||||
$result = core_enrol_external::search_users($course->id, 'user', true, 0, 30, $contextid);
|
||||
$this->assertCount(3, $result);
|
||||
|
||||
$this->setUser($teacher3);
|
||||
$result = core_enrol_external::search_users($course->id, 'user', true, 0, 30, $contextid);
|
||||
$this->assertCount(0, $result);
|
||||
|
||||
$this->setUser($teacher4);
|
||||
$result = core_enrol_external::search_users($course->id, 'user', true, 0, 30, $contextid);
|
||||
$this->assertCount(7, $result);
|
||||
|
||||
// Now change the group mode to no groups.
|
||||
set_coursemodule_groupmode($forum->cmid, NOGROUPS);
|
||||
$this->setUser($teacher1);
|
||||
$result = core_enrol_external::search_users($course->id, 'user', true, 0, 30, $contextid);
|
||||
$this->assertCount(7, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the get_potential_users external function (not too much detail because the back-end
|
||||
* is covered in another test).
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
This files describes API changes in /enrol/* - plugins,
|
||||
information provided here is intended especially for developers.
|
||||
|
||||
=== 4.1.9 ===
|
||||
|
||||
* Functions core_enrol_external::search_users and course_enrolment_manager::search_users now have extra optional parameter
|
||||
contextid which allows to search users in a specific activity context. When omitted it searches in the whole course.
|
||||
|
||||
=== 4.1.1 ===
|
||||
|
||||
* New is_self_enrol_available() function has been created. Similar to can_self_enrol but without checking user capabilities.
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
<?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_grades\external;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
|
||||
require_once($CFG->dirroot . '/grade/lib.php');
|
||||
require_once($CFG->dirroot . '/user/externallib.php');
|
||||
|
||||
use coding_exception;
|
||||
use core_user_external;
|
||||
use external_api;
|
||||
use external_function_parameters;
|
||||
use external_multiple_structure;
|
||||
use external_single_structure;
|
||||
use external_value;
|
||||
use external_warnings;
|
||||
use invalid_parameter_exception;
|
||||
use moodle_exception;
|
||||
use restricted_context_exception;
|
||||
use user_picture;
|
||||
|
||||
/**
|
||||
* Get the gradable users in a course.
|
||||
*
|
||||
* @package core_grades
|
||||
* @copyright 2023 Ilya Tregubov <ilya.a.tregubov@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class get_gradable_users extends external_api {
|
||||
|
||||
/**
|
||||
* Returns description of method parameters.
|
||||
*
|
||||
* @return external_function_parameters
|
||||
*/
|
||||
public static function execute_parameters(): external_function_parameters {
|
||||
return new external_function_parameters (
|
||||
[
|
||||
'courseid' => new external_value(PARAM_INT, 'Course Id', VALUE_REQUIRED),
|
||||
'groupid' => new external_value(PARAM_INT, 'Group Id', VALUE_DEFAULT, 0),
|
||||
'onlyactive' => new external_value(PARAM_BOOL, 'Only active enrolment', VALUE_DEFAULT, false),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a course ID find the gradable users within a group.
|
||||
*
|
||||
* @param int $courseid Course ID
|
||||
* @param int|null $groupid Group ID
|
||||
* @param bool $onlyactive Whether we should only return active enrolments.
|
||||
* @return array Users and warnings.
|
||||
* @throws coding_exception
|
||||
* @throws invalid_parameter_exception
|
||||
* @throws moodle_exception
|
||||
* @throws restricted_context_exception
|
||||
*/
|
||||
public static function execute(int $courseid, ?int $groupid = 0, bool $onlyactive = false): array {
|
||||
global $DB, $PAGE;
|
||||
|
||||
$params = self::validate_parameters(
|
||||
self::execute_parameters(),
|
||||
[
|
||||
'courseid' => $courseid,
|
||||
'groupid' => $groupid,
|
||||
'onlyactive' => $onlyactive,
|
||||
]
|
||||
);
|
||||
|
||||
$warnings = [];
|
||||
$coursecontext = \context_course::instance($params['courseid']);
|
||||
parent::validate_context($coursecontext);
|
||||
|
||||
require_capability('moodle/course:viewparticipants', $coursecontext);
|
||||
|
||||
$course = $DB->get_record('course', ['id' => $params['courseid']]);
|
||||
$onlyactive = $onlyactive || !has_capability('moodle/course:viewsuspendedusers', $coursecontext);
|
||||
|
||||
$users = get_gradable_users($course->id, $params['groupid'], $onlyactive);
|
||||
$users = array_map(function ($user) use ($PAGE) {
|
||||
$user->fullname = fullname($user);
|
||||
$userpicture = new user_picture($user);
|
||||
$userpicture->size = 1;
|
||||
$user->profileimageurlsmall = $userpicture->get_url($PAGE)->out(false);
|
||||
$user->profileimageurl = $userpicture->get_url($PAGE)->out(false);
|
||||
return $user;
|
||||
}, $users);
|
||||
sort($users);
|
||||
|
||||
return [
|
||||
'users' => $users,
|
||||
'warnings' => $warnings,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns description of method result value.
|
||||
*
|
||||
* @return external_single_structure
|
||||
*/
|
||||
public static function execute_returns(): external_single_structure {
|
||||
return new external_single_structure([
|
||||
'users' => new external_multiple_structure(core_user_external::user_description()),
|
||||
'warnings' => new external_warnings(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
@gradingform @gradingform_guide
|
||||
Feature: Teacher can delete marking guide
|
||||
As a teacher,
|
||||
I should be able to delete a marking guide
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | One | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | format |
|
||||
| Course 1 | C1 | topics |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "activities" exist:
|
||||
| activity | course | name | advancedgradingmethod_submissions |
|
||||
| assign | C1 | Assign 1 | guide |
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
And I go to "Assign 1" advanced grading definition page
|
||||
And I set the following fields to these values:
|
||||
| Name | Marking guide 1 |
|
||||
And I define the following marking guide:
|
||||
| Criterion name | Description for students | Description for markers | Maximum score |
|
||||
| Criterion 1 | Criterion 1 description for student | Criterion 1 description for markers | 100 |
|
||||
And I press "Save marking guide and make it ready"
|
||||
|
||||
Scenario: Delete a marking guide
|
||||
Given I am on the "Course 1" course page
|
||||
And I go to "Assign 1" advanced grading page
|
||||
When I click on "Delete the currently defined form" "link"
|
||||
And I press "Cancel"
|
||||
# Confirm that marking guide was not deleted if Cancel is pressed
|
||||
Then I should see "Marking guide 1 Ready for use"
|
||||
And I should see "Criterion 1"
|
||||
And I click on "Delete the currently defined form" "link"
|
||||
And I press "Continue"
|
||||
# Confirm that marking guide was deleted successfully if Continue is pressed
|
||||
And 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 not see "Marking guide 1 Ready for use"
|
||||
And I should not see "Criterion 1"
|
||||
@@ -149,27 +149,31 @@ class gradeimport_csv_load_data {
|
||||
/**
|
||||
* Inserts a record into the grade_import_values table. This also adds common record information.
|
||||
*
|
||||
* @param object $record The grade record being inserted into the database.
|
||||
* @param stdClass $record The grade record being inserted into the database.
|
||||
* @param int $studentid The student ID.
|
||||
* @return bool|int true or insert id on success. Null if the grade value is too high.
|
||||
* @param grade_item $gradeitem Grade item.
|
||||
* @return bool|int true or insert id on success. Null if the grade value is too high or too low or grade item not exist.
|
||||
*/
|
||||
protected function insert_grade_record($record, $studentid) {
|
||||
protected function insert_grade_record(stdClass $record, int $studentid, grade_item $gradeitem) {
|
||||
global $DB, $USER, $CFG;
|
||||
$record->importcode = $this->importcode;
|
||||
$record->userid = $studentid;
|
||||
$record->importer = $USER->id;
|
||||
// By default the maximum grade is 100.
|
||||
$gradepointmaximum = 100;
|
||||
// If the grade limit has been increased then use the gradepointmax setting.
|
||||
if ($CFG->unlimitedgrades) {
|
||||
$gradepointmaximum = $CFG->gradepointmax;
|
||||
}
|
||||
// If the record final grade is set then check that the grade value isn't too high.
|
||||
// Final grade will not be set if we are inserting feedback.
|
||||
if (!isset($record->finalgrade) || $record->finalgrade <= $gradepointmaximum) {
|
||||
$gradepointmaximum = $gradeitem->grademax;
|
||||
$gradepointminimum = $gradeitem->grademin;
|
||||
|
||||
$finalgradeinrange =
|
||||
isset($record->finalgrade) && $record->finalgrade <= $gradepointmaximum && $record->finalgrade >= $gradepointminimum;
|
||||
if (!isset($record->finalgrade) || $finalgradeinrange || $CFG->unlimitedgrades) {
|
||||
return $DB->insert_record('grade_import_values', $record);
|
||||
} else {
|
||||
$this->cleanup_import(get_string('gradevaluetoobig', 'grades', $gradepointmaximum));
|
||||
if ($record->finalgrade > $gradepointmaximum) {
|
||||
$this->cleanup_import(get_string('gradevaluetoobig', 'grades', format_float($gradepointmaximum)));
|
||||
} else {
|
||||
$this->cleanup_import(get_string('gradevaluetoosmall', 'grades', format_float($gradepointminimum)));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -575,7 +579,12 @@ class gradeimport_csv_load_data {
|
||||
}
|
||||
}
|
||||
}
|
||||
$insertid = self::insert_grade_record($newgrade, $this->studentid);
|
||||
if (isset($newgrade->itemid)) {
|
||||
$gradeitem = new grade_item(['id' => $newgrade->itemid]);
|
||||
} else if (isset($newgrade->newgradeitem)) {
|
||||
$gradeitem = new grade_item(['id' => $newgrade->newgradeitem]);
|
||||
}
|
||||
$insertid = isset($gradeitem) ? self::insert_grade_record($newgrade, $this->studentid, $gradeitem) : null;
|
||||
// Check to see if the insert was successful.
|
||||
if (empty($insertid)) {
|
||||
return null;
|
||||
@@ -597,7 +606,7 @@ class gradeimport_csv_load_data {
|
||||
} else {
|
||||
// The grade item for this is not updated.
|
||||
$newfeedback->importonlyfeedback = true;
|
||||
$insertid = self::insert_grade_record($newfeedback, $this->studentid);
|
||||
$insertid = self::insert_grade_record($newfeedback, $this->studentid, new grade_item(['id' => $newfeedback->itemid]));
|
||||
// Check to see if the insert was successful.
|
||||
if (empty($insertid)) {
|
||||
return null;
|
||||
|
||||
@@ -27,10 +27,11 @@ class phpunit_gradeimport_csv_load_data extends gradeimport_csv_load_data {
|
||||
*
|
||||
* @param object $record
|
||||
* @param int $studentid
|
||||
* @param grade_item $gradeitem
|
||||
*/
|
||||
public function test_insert_grade_record($record, $studentid) {
|
||||
public function test_insert_grade_record($record, $studentid, grade_item $gradeitem) {
|
||||
$this->importcode = 00001;
|
||||
$this->insert_grade_record($record, $studentid);
|
||||
$this->insert_grade_record($record, $studentid, $gradeitem);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -187,7 +187,8 @@ Bobby,Bunce,,"Moodle HQ","Rock on!",student5@example.com,75.00,,75.00,{exportdat
|
||||
$record->feedback = 'Some test feedback';
|
||||
|
||||
$testobject = new \phpunit_gradeimport_csv_load_data();
|
||||
$testobject->test_insert_grade_record($record, $user->id);
|
||||
|
||||
$testobject->test_insert_grade_record($record, $user->id, new \grade_item());
|
||||
|
||||
$gradeimportvalues = $DB->get_records('grade_import_values');
|
||||
// Get the insert id.
|
||||
|
||||
+21
-11
@@ -786,18 +786,12 @@ function grade_get_plugin_info($courseid, $active_type, $active_plugin) {
|
||||
*
|
||||
* @param int $courseid The course ID.
|
||||
* @param int|null $groupid The group ID (optional).
|
||||
* @param bool $onlyactiveenrol Include only active enrolments.
|
||||
* @return array $users A list of enrolled gradable users.
|
||||
*/
|
||||
function get_gradable_users(int $courseid, ?int $groupid = null): array {
|
||||
global $CFG;
|
||||
|
||||
$context = context_course::instance($courseid);
|
||||
// Create a graded_users_iterator because it will properly check the groups etc.
|
||||
$defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
|
||||
$onlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol) ||
|
||||
!has_capability('moodle/course:viewsuspendedusers', $context);
|
||||
|
||||
function get_gradable_users(int $courseid, ?int $groupid = null, bool $onlyactiveenrol = false): array {
|
||||
$course = get_course($courseid);
|
||||
// Create a graded_users_iterator because it will properly check the groups etc.
|
||||
$gui = new graded_users_iterator($course, null, $groupid);
|
||||
$gui->require_active_enrolment($onlyactiveenrol);
|
||||
$gui->init();
|
||||
@@ -891,6 +885,12 @@ function print_grade_page_head(int $courseid, string $active_type, ?string $acti
|
||||
?string $headerhelpcomponent = null, ?stdClass $user = null, ?action_bar $actionbar = null, $showtitle = true) {
|
||||
global $CFG, $OUTPUT, $PAGE, $USER;
|
||||
|
||||
if ($heading !== false) {
|
||||
// Make sure to trim heading, including the non-breaking space character.
|
||||
$heading = str_replace(" ", " ", $heading);
|
||||
$heading = trim($heading);
|
||||
}
|
||||
|
||||
// Put a warning on all gradebook pages if the course has modules currently scheduled for background deletion.
|
||||
require_once($CFG->dirroot . '/course/lib.php');
|
||||
if (course_modules_pending_deletion($courseid, true)) {
|
||||
@@ -924,14 +924,24 @@ function print_grade_page_head(int $courseid, string $active_type, ?string $acti
|
||||
}
|
||||
$coursecontext = context_course::instance($courseid);
|
||||
// Title will be constituted by information starting from the unique identifying information for the page.
|
||||
if (in_array($active_type, ['report', 'settings'])) {
|
||||
if ($heading) {
|
||||
// If heading is supplied, use this for the page title.
|
||||
$uniquetitle = $heading;
|
||||
} else if (in_array($active_type, ['report', 'settings'])) {
|
||||
// For grade reports or settings pages of grade plugins, use the plugin name for the unique title.
|
||||
$uniquetitle = $stractive_plugin;
|
||||
// But if editing mode is turned on, check if the report plugin has an editing mode title string and use it if present.
|
||||
if ($PAGE->user_is_editing() && $active_type === 'report') {
|
||||
$strcomponent = "gradereport_{$active_plugin}";
|
||||
if (get_string_manager()->string_exists('editingmode_title', $strcomponent)) {
|
||||
$uniquetitle = get_string('editingmode_title', $strcomponent);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$uniquetitle = $stractive_type . ': ' . $stractive_plugin;
|
||||
}
|
||||
$titlecomponents = [
|
||||
$uniquetitle,
|
||||
get_string('grades'),
|
||||
$coursecontext->get_context_name(false),
|
||||
];
|
||||
$PAGE->set_title(implode(moodle_page::TITLE_SEPARATOR, $titlecomponents));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user