Compare commits
299 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e591ddc4e8 | |||
| e1767fa80f | |||
| e25033e42d | |||
| dd2c19eb09 | |||
| 3b8b8d7d84 | |||
| 5378285a9a | |||
| 3a098250f1 | |||
| 148ddd26e7 | |||
| cdf432ee17 | |||
| b1e918edd8 | |||
| f58df6fa88 | |||
| eae562817d | |||
| 033a59f9c5 | |||
| a34a8636be | |||
| 08d2e7c136 | |||
| 886bbcbdb8 | |||
| 0c3e759d98 | |||
| 607a452a74 | |||
| 3ef6e04f3c | |||
| dfc7dcd062 | |||
| 80d773868c | |||
| eb127cc93b | |||
| 86716dc120 | |||
| 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 | |||
| 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
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 I should see "Course 3" in the "h1" "css_element"
|
||||
And I switch to "CourseViewer1" tab
|
||||
And I should see "Course 2" in the "h1" "css_element"
|
||||
And I switch to the main tab
|
||||
And I should see "Course 1" in the "h1" "css_element"
|
||||
+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 >}}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,7 @@ require_sesskey();
|
||||
|
||||
// Prepare to handle output via mtrace.
|
||||
echo html_writer::start_tag('pre');
|
||||
require('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 |
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
+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();
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 | Frist | 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"
|
||||
|
||||
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>
|
||||
|
||||
@@ -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,11 @@
|
||||
This files describes API changes in /course/*,
|
||||
information provided here is intended especially for developers.
|
||||
|
||||
=== 4.1.7 ===
|
||||
* 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();
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
+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));
|
||||
|
||||
@@ -107,13 +107,11 @@ if (!empty($target) && !empty($action) && confirm_sesskey()) {
|
||||
grade_report_grader::do_process_action($target, $action, $courseid);
|
||||
}
|
||||
|
||||
$reportname = get_string('pluginname', 'gradereport_grader');
|
||||
|
||||
// Do this check just before printing the grade header (and only do it once).
|
||||
grade_regrade_final_grades_if_required($course);
|
||||
|
||||
// Print header
|
||||
print_grade_page_head($COURSE->id, 'report', 'grader', $reportname, false, $buttons);
|
||||
print_grade_page_head($COURSE->id, 'report', 'grader', false, false, $buttons);
|
||||
|
||||
//Initialise the grader report object that produces the table
|
||||
//the class grade_report_grader_ajax was removed as part of MDL-21562
|
||||
|
||||
@@ -28,6 +28,7 @@ $string['ajaxerror'] = 'Error';
|
||||
$string['ajaxfailedupdate'] = 'Unable to update [1] for [2]';
|
||||
$string['ajaxfieldchanged'] = 'The field you are currently editing has changed, would you like to use the updated value?';
|
||||
$string['ajaxchoosescale'] = 'Choose';
|
||||
$string['editingmode_title'] = 'Editing the grader report';
|
||||
$string['eventgradereportviewed'] = 'Grader report viewed';
|
||||
$string['grader:manage'] = 'Manage the grader report';
|
||||
$string['grader:view'] = 'View grader report';
|
||||
|
||||
+29
-11
@@ -802,8 +802,6 @@ class grade_report_grader extends grade_report {
|
||||
$rows = array();
|
||||
$this->rowcount = 0;
|
||||
$numrows = count($this->gtree->get_levels());
|
||||
$numusers = count($this->users);
|
||||
$gradetabindex = 1;
|
||||
$columnstounset = array();
|
||||
$strgrade = $this->get_lang_string('gradenoun');
|
||||
$strfeedback = $this->get_lang_string("feedback");
|
||||
@@ -945,7 +943,6 @@ class grade_report_grader extends grade_report {
|
||||
|
||||
// Preload scale objects for items with a scaleid and initialize tab indices
|
||||
$scaleslist = array();
|
||||
$tabindices = array();
|
||||
|
||||
foreach ($this->gtree->get_items() as $itemid => $item) {
|
||||
$scale = null;
|
||||
@@ -955,9 +952,6 @@ class grade_report_grader extends grade_report {
|
||||
} else {
|
||||
$jsarguments['items'][$itemid] = array('id'=>$itemid, 'name'=>$item->get_name(true), 'type'=>'value', 'scale'=>false, 'decimals'=>$item->get_decimals());
|
||||
}
|
||||
$tabindices[$item->id]['grade'] = $gradetabindex;
|
||||
$tabindices[$item->id]['feedback'] = $gradetabindex + $numusers;
|
||||
$gradetabindex += $numusers * 2;
|
||||
}
|
||||
$scalesarray = array();
|
||||
|
||||
@@ -1123,7 +1117,7 @@ class grade_report_grader extends grade_report {
|
||||
} else {
|
||||
$nogradestr = $this->get_lang_string('nooutcome', 'grades');
|
||||
}
|
||||
$attributes = array('tabindex' => $tabindices[$item->id]['grade'], 'id'=>'grade_'.$userid.'_'.$item->id);
|
||||
$attributes = ['id' => 'grade_' . $userid . '_' . $item->id];
|
||||
$gradelabel = $fullname . ' ' . $item->get_name(true);
|
||||
$itemcell->text .= html_writer::label(
|
||||
get_string('useractivitygrade', 'gradereport_grader', $gradelabel), $attributes['id'], false,
|
||||
@@ -1149,9 +1143,33 @@ class grade_report_grader extends grade_report {
|
||||
$gradelabel = $fullname . ' ' . $item->get_name(true);
|
||||
$itemcell->text .= '<label class="accesshide" for="grade_'.$userid.'_'.$item->id.'">'
|
||||
.get_string('useractivitygrade', 'gradereport_grader', $gradelabel).'</label>';
|
||||
$itemcell->text .= '<input size="6" tabindex="' . $tabindices[$item->id]['grade']
|
||||
. '" type="text" class="text" title="'. $strgrade .'" name="grade['
|
||||
.$userid.'][' .$item->id.']" id="grade_'.$userid.'_'.$item->id.'" value="'.$value.'" />';
|
||||
|
||||
// Set this input field with type="number" if the decimal separator for current language is set to
|
||||
// a period. Other decimal separators may not be recognised by browsers yet which may cause issues
|
||||
// when entering grades.
|
||||
$decsep = get_string('decsep', 'core_langconfig');
|
||||
$isnumeric = $decsep === '.';
|
||||
$inputtype = $isnumeric ? 'number' : 'text';
|
||||
$inputparams = [
|
||||
'type' => $inputtype,
|
||||
'class' => 'text',
|
||||
'title' => $strgrade,
|
||||
'name' => "grade[{$userid}][{$item->id}]",
|
||||
'id' => "grade_{$userid}_{$item->id}",
|
||||
'value' => $value,
|
||||
];
|
||||
// If we're rendering this as a number field, set step and min/max attributes (if applicable).
|
||||
if ($isnumeric) {
|
||||
$inputparams['step'] = 'any';
|
||||
if (isset($item->grademin)) {
|
||||
$inputparams['min'] = $item->grademin;
|
||||
}
|
||||
if (isset($item->grademax)) {
|
||||
$inputparams['max'] = $item->grademax;
|
||||
}
|
||||
}
|
||||
|
||||
$itemcell->text .= html_writer::empty_tag('input', $inputparams);
|
||||
} else {
|
||||
$itemcell->text .= $gradepassicon . "<span class='gradevalue{$hidden}{$gradepass}'>" .
|
||||
format_float($gradeval, $decimalpoints) . "</span>";
|
||||
@@ -1163,7 +1181,7 @@ class grade_report_grader extends grade_report {
|
||||
$feedbacklabel = $fullname . ' ' . $item->get_name(true);
|
||||
$itemcell->text .= '<label class="accesshide" for="feedback_'.$userid.'_'.$item->id.'">'
|
||||
.get_string('useractivityfeedback', 'gradereport_grader', $feedbacklabel).'</label>';
|
||||
$itemcell->text .= '<input class="quickfeedback" tabindex="' . $tabindices[$item->id]['feedback'].'" id="feedback_'.$userid.'_'.$item->id
|
||||
$itemcell->text .= '<input class="quickfeedback" id="feedback_'.$userid.'_'.$item->id
|
||||
. '" size="6" title="' . $strfeedback . '" type="text" name="feedback['.$userid.']['.$item->id.']" value="' . s($grade->feedback) . '" />';
|
||||
}
|
||||
|
||||
|
||||
@@ -755,5 +755,25 @@ abstract class grade_report {
|
||||
|
||||
return $modnames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a valid list of gradable users in a course.
|
||||
*
|
||||
* @param int $courseid The course ID.
|
||||
* @param int|null $groupid The group ID (optional).
|
||||
* @return array A list of enrolled gradable users.
|
||||
*/
|
||||
public static function get_gradable_users(int $courseid, ?int $groupid = null): array {
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/grade/lib.php');
|
||||
|
||||
$context = context_course::instance($courseid);
|
||||
$defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol);
|
||||
$onlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol) ||
|
||||
!has_capability('moodle/course:viewsuspendedusers', $context);
|
||||
|
||||
return get_gradable_users($courseid, $groupid, $onlyactiveenrol);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -80,14 +80,17 @@ class grade_report_overview extends grade_report {
|
||||
* @param string $context
|
||||
*/
|
||||
public function __construct($userid, $gpr, $context) {
|
||||
global $CFG, $COURSE, $DB;
|
||||
global $CFG, $COURSE, $DB, $USER;
|
||||
parent::__construct($COURSE->id, $gpr, $context);
|
||||
|
||||
// Get the user (for full name).
|
||||
$this->user = $DB->get_record('user', array('id' => $userid));
|
||||
|
||||
// Set onlyactive flag to true if the user's viewing his/her report.
|
||||
$onlyactive = ($this->user->id === $USER->id);
|
||||
|
||||
// Load the user's courses.
|
||||
$this->courses = enrol_get_users_courses($this->user->id, false, 'id, shortname, showgrades');
|
||||
$this->courses = enrol_get_users_courses($this->user->id, $onlyactive, 'id, shortname, showgrades');
|
||||
|
||||
$this->showrank = array();
|
||||
$this->showrank['any'] = false;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
@gradereport @gradereport_overview
|
||||
Feature: Grade overview report should be hidden from suspended enrolments
|
||||
While viewing the grade overview report
|
||||
As a student
|
||||
I should only see courses I am active in
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Active course | C1 |
|
||||
| Suspended course | C2 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| student1 | Student | 1 | student1@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role | status |
|
||||
| student1 | C1 | student | 0 |
|
||||
| student1 | C2 | student | 1 |
|
||||
|
||||
Scenario: Students should not see grades for courses with suspended enrolments
|
||||
Given I am on the "Active course" "grades > Overview report > View" page logged in as "student1"
|
||||
Then I should not see "Suspended course" in the "overview-grade" "table"
|
||||
And I should see "Active course" in the "overview-grade" "table"
|
||||
|
||||
Scenario: Admins should see courses with suspended enrolments
|
||||
Given I log in as "admin"
|
||||
And I navigate to "Users > Accounts > Browse list of users" in site administration
|
||||
And I follow "Student 1"
|
||||
When I click on "Grades overview" "link"
|
||||
Then I should see "Suspended course" in the "overview-grade" "table"
|
||||
And I should see "Active course" in the "overview-grade" "table"
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
namespace gradereport_singleview\local\screen;
|
||||
|
||||
use grade_report;
|
||||
use gradereport_singleview\local\ui\range;
|
||||
use gradereport_singleview\local\ui\bulk_insert;
|
||||
use grade_grade;
|
||||
@@ -139,7 +140,7 @@ class grade extends tablelike implements selectable_items, filterable_items {
|
||||
*/
|
||||
public function init($selfitemisempty = false) {
|
||||
|
||||
$this->items = $this->load_users();
|
||||
$this->items = grade_report::get_gradable_users($this->courseid, $this->groupid);
|
||||
$this->totalitemcount = count($this->items);
|
||||
|
||||
if ($selfitemisempty) {
|
||||
@@ -301,7 +302,9 @@ class grade extends tablelike implements selectable_items, filterable_items {
|
||||
* @return string
|
||||
*/
|
||||
public function heading(): string {
|
||||
return get_string('gradeitem', 'gradereport_singleview', $this->item->get_name());
|
||||
global $PAGE;
|
||||
$headinglangstring = $PAGE->user_is_editing() ? 'gradeitemedit' : 'gradeitem';
|
||||
return get_string($headinglangstring, 'gradereport_singleview', $this->item->get_name());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
namespace gradereport_singleview\local\screen;
|
||||
|
||||
use context_course;
|
||||
use grade_report;
|
||||
use moodle_url;
|
||||
use html_writer;
|
||||
use grade_structure;
|
||||
@@ -33,6 +34,7 @@ use grade_item;
|
||||
use stdClass;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
require_once($CFG->dirroot . '/grade/report/lib.php');
|
||||
|
||||
/**
|
||||
* Abstract class used as a base for the 3 screens.
|
||||
|
||||
@@ -99,7 +99,7 @@ class user extends tablelike implements selectable_items {
|
||||
public function init($selfitemisempty = false) {
|
||||
|
||||
if (!$selfitemisempty) {
|
||||
$validusers = $this->load_users();
|
||||
$validusers = \grade_report::get_gradable_users($this->courseid, $this->groupid);
|
||||
if (!isset($validusers[$this->itemid])) {
|
||||
// If the passed user id is not valid, show the first user from the list instead.
|
||||
$this->item = reset($validusers);
|
||||
@@ -302,7 +302,9 @@ class user extends tablelike implements selectable_items {
|
||||
* @return string
|
||||
*/
|
||||
public function heading(): string {
|
||||
return get_string('gradeuser', 'gradereport_singleview', fullname($this->item));
|
||||
global $PAGE;
|
||||
$headinglangstring = $PAGE->user_is_editing() ? 'gradeuseredit' : 'gradeuser';
|
||||
return get_string($headinglangstring, 'gradereport_singleview', fullname($this->item));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,6 +27,7 @@ define('NO_OUTPUT_BUFFERING', true);
|
||||
require_once('../../../config.php');
|
||||
require_once($CFG->dirroot.'/lib/gradelib.php');
|
||||
require_once($CFG->dirroot.'/grade/lib.php');
|
||||
require_once($CFG->dirroot.'/grade/report/lib.php');
|
||||
|
||||
$courseid = required_param('id', PARAM_INT);
|
||||
$groupid = optional_param('group', null, PARAM_INT);
|
||||
@@ -90,7 +91,8 @@ switch ($itemtype) {
|
||||
// If there is a stored user item (last viewed) in a session variable, bypass the user select zero state
|
||||
// and display this user item. Also, make sure that the stored last viewed user is part of the current
|
||||
// list of gradable users in this course.
|
||||
if ($lastvieweduseritemid && array_key_exists($lastvieweduseritemid, get_gradable_users($courseid, $currentgroup))) {
|
||||
if ($lastvieweduseritemid &&
|
||||
array_key_exists($lastvieweduseritemid, grade_report::get_gradable_users($courseid, $currentgroup))) {
|
||||
$itemtype = 'user';
|
||||
$itemid = $lastvieweduseritemid;
|
||||
} else {
|
||||
@@ -103,7 +105,7 @@ switch ($itemtype) {
|
||||
}
|
||||
// If the item id (user id) cannot be defined or the user id is not part of the list of gradable users,
|
||||
// display the user select zero state.
|
||||
if (is_null($itemid) || !array_key_exists($itemid, get_gradable_users($courseid, $currentgroup))) {
|
||||
if (is_null($itemid) || !array_key_exists($itemid, grade_report::get_gradable_users($courseid, $currentgroup))) {
|
||||
$itemtype = 'user_select';
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -44,8 +44,10 @@ $string['excludenonegrades'] = 'Exclude none';
|
||||
$string['eventgradereportviewed'] = 'Grade single view report viewed.';
|
||||
$string['feedbackfor'] = 'Feedback for {$a}';
|
||||
$string['gradefor'] = 'Grade for {$a}';
|
||||
$string['gradeitem'] = 'Grade item: {$a}';
|
||||
$string['gradeuser'] = 'Grade user: {$a}';
|
||||
$string['gradeitem'] = 'Viewing grades for {$a}';
|
||||
$string['gradeitemedit'] = 'Editing grades for {$a}';
|
||||
$string['gradeuser'] = 'Viewing {$a}\'s grades';
|
||||
$string['gradeuseredit'] = 'Editing {$a}\'s grades';
|
||||
$string['gotonextreport'] = 'Go to next user report';
|
||||
$string['gotopreviousreport'] = 'Go to previous user report';
|
||||
$string['noscreens'] = 'Could not find a suitable single view screen.';
|
||||
|
||||
@@ -26,6 +26,6 @@
|
||||
}}
|
||||
<div class="zero-state mx-auto w-50 text-center my-6">
|
||||
<img src="{{imglink}}" alt="{{#str}}viewsinglegradeitem, gradereport_singleview{{/str}}" aria-hidden="true" class="my-5">
|
||||
<h3>{{#str}}viewsinglegradeitem, gradereport_singleview{{/str}}</h3>
|
||||
<h2 class="h3">{{#str}}viewsinglegradeitem, gradereport_singleview{{/str}}</h2>
|
||||
<p>{{#str}}singleviewdescription, gradereport_singleview{{/str}}</p>
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,6 @@
|
||||
}}
|
||||
<div class="zero-state mx-auto w-50 text-center my-6">
|
||||
<img src="{{imglink}}" alt="{{#str}}viewsingleuser, gradereport_singleview{{/str}}" aria-hidden="true" class="my-5">
|
||||
<h3>{{#str}}viewsingleuser, gradereport_singleview{{/str}}</h3>
|
||||
<h2 class="h3">{{#str}}viewsingleuser, gradereport_singleview{{/str}}</h2>
|
||||
<p>{{#str}}singleviewdescription, gradereport_singleview{{/str}}</p>
|
||||
</div>
|
||||
|
||||
@@ -113,7 +113,7 @@ if (has_capability('moodle/grade:viewall', $context)) {
|
||||
$userid = $lastvieweduserid;
|
||||
}
|
||||
|
||||
$gradableusers = get_gradable_users($courseid, $currentgroup);
|
||||
$gradableusers = grade_report::get_gradable_users($courseid, $currentgroup);
|
||||
// Validate whether the requested user is a valid gradable user in this course. If, not display the user select
|
||||
// zero state.
|
||||
if (empty($gradableusers) || ($userid && !array_key_exists($userid, $gradableusers))) {
|
||||
|
||||
@@ -43,9 +43,9 @@ Feature: Site settings can be used to hide parts of the gradebook UI
|
||||
|
||||
@javascript
|
||||
Scenario: Disable category overriding
|
||||
And "tr .course input[type='text']" "css_element" should exist
|
||||
Then I navigate to "Grades > Grade category settings" in site administration
|
||||
Given "Student 1 Course total" "field" should exist
|
||||
And I navigate to "Grades > Grade category settings" in site administration
|
||||
And I set the field "Allow category grades to be manually overridden" to "0"
|
||||
And I press "Save changes"
|
||||
And I am on the "Course 1" "grades > Grader report > View" page
|
||||
And "tr .course input[type='text']" "css_element" should not exist
|
||||
When I am on the "Course 1" "grades > Grader report > View" page
|
||||
Then "Student 1 Course total" "field" should not exist
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
@core @core_grades @javascript @_file_upload
|
||||
Feature: An admin can import grades into gradebook using a CSV file
|
||||
In order to import grades using a CSV file
|
||||
As a teacher
|
||||
I need to be able to upload a CSV file and see uploaded grades in gradebook
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | format |
|
||||
| Course 1 | C1 | topics |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
| student1 | Student | 1 | student1@example.com |
|
||||
| student2 | Student | 2 | student2@example.com |
|
||||
| student3 | Student | 3 | student3@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
| student2 | C1 | student |
|
||||
| student3 | C1 | student |
|
||||
And the following "grade item" exists:
|
||||
| course | C1 |
|
||||
| itemname | Manual item 1 |
|
||||
| grademin | 10 |
|
||||
| grademax | 500 |
|
||||
And the following "grade grades" exist:
|
||||
| gradeitem | user | grade |
|
||||
| Manual item 1 | student1 | 50.00 |
|
||||
| Manual item 1 | student2 | 50.00 |
|
||||
| Manual item 1 | student3 | 50.00 |
|
||||
|
||||
@javascript
|
||||
Scenario: Max grade of grade item is respected when importing grades
|
||||
Given I am on the "Course 1" "Course" page logged in as "teacher1"
|
||||
And I navigate to "More > Import" in the course gradebook
|
||||
And I upload "grade/tests/fixtures/grade_import_grademax.csv" file to "File" filemanager
|
||||
And I click on "Upload grades" "button"
|
||||
And I set the field "Map from" to "Email address"
|
||||
And I set the field "Map to" to "Email address"
|
||||
And I set the field "Manual item 1" to "Manual item 1"
|
||||
And I click on "Upload grades" "button"
|
||||
And I should see "One of the grade values is larger than the allowed grade maximum of 500.0"
|
||||
And I should see "Import failed. No data was imported."
|
||||
And I click on "Continue" "button"
|
||||
And I upload "grade/tests/fixtures/grade_import_grademin.csv" file to "File" filemanager
|
||||
And I click on "Upload grades" "button"
|
||||
And I set the field "Map from" to "Email address"
|
||||
And I set the field "Map to" to "Email address"
|
||||
And I set the field "Manual item 1" to "Manual item 1"
|
||||
And I click on "Upload grades" "button"
|
||||
And I should see "One of the grade values is smaller than the allowed grade minimum of 10.0"
|
||||
And I should see "Import failed. No data was imported."
|
||||
And I click on "Continue" "button"
|
||||
When I upload "grade/tests/fixtures/grade_import.csv" file to "File" filemanager
|
||||
And I click on "Upload grades" "button"
|
||||
And I set the field "Map from" to "Email address"
|
||||
And I set the field "Map to" to "Email address"
|
||||
And I set the field "Manual item 1" to "Manual item 1"
|
||||
And I click on "Upload grades" "button"
|
||||
And I should see "Grade import success"
|
||||
And I click on "Continue" "button"
|
||||
Then the following should exist in the "user-grades" table:
|
||||
| -1- | -1- | -4- | -5- |
|
||||
| Student 1 | student1@example.com | 400.00 | 400.00 |
|
||||
| Student 2 | student2@example.com | 50.00 | 50.00 |
|
||||
| Student 3 | student3@example.com | 50.00 | 50.00 |
|
||||
@@ -19,50 +19,23 @@ Feature: We can use a minimum grade different than zero
|
||||
| student1 | C1 | student |
|
||||
| student2 | C1 | student |
|
||||
And the following "grade categories" exist:
|
||||
| fullname | course |
|
||||
| Sub category 1 | C1 |
|
||||
| Sub category 2 | C1 |
|
||||
| fullname | course | aggregateonlygraded |
|
||||
| Sub category 1 | C1 | 0 |
|
||||
| Sub category 2 | C1 | 0 |
|
||||
And the following "grade items" exist:
|
||||
| itemname | grademin | course |
|
||||
| Manual item 1 | -100 | C1 |
|
||||
| Manual item 2 | 50 | C1 |
|
||||
And the following "grade items" exist:
|
||||
| itemname | grademin | grademax | course | gradecategory |
|
||||
| Manual item 3 | -100 | 50 | C1 | Sub category 1 |
|
||||
And the following "grade items" exist:
|
||||
| itemname | grademin | course | gradecategory |
|
||||
| Manual item 4 | -100 | C1 | Sub category 1 |
|
||||
| Manual item 5 | 50 | C1 | Sub category 2 |
|
||||
| Manual item 6 | 50 | C1 | Sub category 2 |
|
||||
And I log in as "admin"
|
||||
And I set the following administration settings values:
|
||||
| grade_aggregations_visible | Mean of grades,Weighted mean of grades,Simple weighted mean of grades,Mean of grades (with extra credits),Median of grades,Lowest grade,Highest grade,Mode of grades,Natural |
|
||||
And I am on the "Course 1" "grades > gradebook setup" page
|
||||
And I press "Add grade item"
|
||||
And I set the following fields to these values:
|
||||
| Item name | Manual item 1 |
|
||||
| Minimum grade | -100 |
|
||||
| Grade category | Course 1 |
|
||||
And I press "Save changes"
|
||||
And I press "Add grade item"
|
||||
And I set the following fields to these values:
|
||||
| Item name | Manual item 2 |
|
||||
| Minimum grade | 50 |
|
||||
| Grade category | Course 1 |
|
||||
And I press "Save changes"
|
||||
And I press "Add grade item"
|
||||
And I set the following fields to these values:
|
||||
| Item name | Manual item 3 |
|
||||
| Maximum grade | 50 |
|
||||
| Minimum grade | -100 |
|
||||
| Grade category | Sub category 1 |
|
||||
And I press "Save changes"
|
||||
And I press "Add grade item"
|
||||
And I set the following fields to these values:
|
||||
| Item name | Manual item 4 |
|
||||
| Minimum grade | -100 |
|
||||
| Grade category | Sub category 1 |
|
||||
And I press "Save changes"
|
||||
And I press "Add grade item"
|
||||
And I set the following fields to these values:
|
||||
| Item name | Manual item 5 |
|
||||
| Minimum grade | 50 |
|
||||
| Grade category | Sub category 2 |
|
||||
And I press "Save changes"
|
||||
And I press "Add grade item"
|
||||
And I set the following fields to these values:
|
||||
| Item name | Manual item 6 |
|
||||
| Minimum grade | 50 |
|
||||
| Grade category | Sub category 2 |
|
||||
And I press "Save changes"
|
||||
And I navigate to "Setup > Course grade settings" in the course gradebook
|
||||
And I set the field "Show weightings" to "Show"
|
||||
And I set the field "Show contribution to course total" to "Show"
|
||||
@@ -70,13 +43,7 @@ Feature: We can use a minimum grade different than zero
|
||||
|
||||
@javascript
|
||||
Scenario: Natural aggregation with negative and positive grade
|
||||
And I navigate to "Setup > Gradebook setup" in the course gradebook
|
||||
And I set the following settings for grade item "Sub category 1":
|
||||
| Aggregation | Natural |
|
||||
| Exclude empty grades | 0 |
|
||||
And I set the following settings for grade item "Sub category 2":
|
||||
| Aggregation | Natural |
|
||||
| Exclude empty grades | 0 |
|
||||
Given I navigate to "Setup > Gradebook setup" in the course gradebook
|
||||
And I set the following settings for grade item "Course 1":
|
||||
| Aggregation | Natural |
|
||||
| Exclude empty grades | 0 |
|
||||
@@ -89,11 +56,11 @@ Feature: We can use a minimum grade different than zero
|
||||
And I give the grade "50.00" to the user "Student 1" for the grade item "Manual item 5"
|
||||
And I give the grade "75.00" to the user "Student 1" for the grade item "Manual item 6"
|
||||
And I give the grade "0.00" to the user "Student 2" for the grade item "Manual item 1"
|
||||
And I give the grade "0.00" to the user "Student 2" for the grade item "Manual item 2"
|
||||
And I give the grade "50.00" to the user "Student 2" for the grade item "Manual item 2"
|
||||
And I give the grade "-10.00" to the user "Student 2" for the grade item "Manual item 3"
|
||||
And I give the grade "50.00" to the user "Student 2" for the grade item "Manual item 4"
|
||||
And I give the grade "0.00" to the user "Student 2" for the grade item "Manual item 5"
|
||||
And I give the grade "0.00" to the user "Student 2" for the grade item "Manual item 6"
|
||||
And I give the grade "50.00" to the user "Student 2" for the grade item "Manual item 5"
|
||||
And I give the grade "50.00" to the user "Student 2" for the grade item "Manual item 6"
|
||||
And I press "Save changes"
|
||||
And I navigate to "View > User report" in the course gradebook
|
||||
And I click on "Student 1" in the "user" search widget
|
||||
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
<?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;
|
||||
|
||||
use external_api;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
|
||||
global $CFG;
|
||||
|
||||
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
|
||||
|
||||
/**
|
||||
* Unit tests for the core_grades\external\get_gradable_users.
|
||||
*
|
||||
* @package core_grades
|
||||
* @category external
|
||||
* @copyright 2023 Ilya Tregubov <ilya.a.tregubov@gmail.com>
|
||||
* @covers \core_grades\external\get_gradable_users
|
||||
*/
|
||||
class get_gradable_users_test extends \externallib_advanced_testcase {
|
||||
|
||||
/**
|
||||
* Test the behaviour of get_gradable_users.
|
||||
*
|
||||
* @dataProvider execute_data
|
||||
* @param bool $onlyactiveenrol if we should only return active enrolments
|
||||
* @param bool $grouprestricted if we should only return users within a group
|
||||
* @param array $expected expected users
|
||||
*/
|
||||
public function test_execute(bool $onlyactiveenrol, bool $grouprestricted, array $expected): void {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
$generator = $this->getDataGenerator();
|
||||
$course = $generator->create_course();
|
||||
|
||||
// Create and enrol test users.
|
||||
$student1 = $generator->create_user(['username' => 'student1', 'firstname' => 'Apple', 'lastname' => 'Apricot']);
|
||||
$student2 = $generator->create_user(['username' => 'student2', 'firstname' => 'Banana', 'lastname' => 'Blueberry']);
|
||||
$student3 = $generator->create_user(['username' => 'student3', 'firstname' => 'Cherry', 'lastname' => 'Cranberry']);
|
||||
$student4 = $generator->create_user(['username' => 'student4', 'firstname' => 'Durian', 'lastname' => 'Dracontomelon']);
|
||||
$student5 = $generator->create_user(['username' => 'student5', 'firstname' => 'Eggplant', 'lastname' => 'Ensete']);
|
||||
|
||||
$role = $DB->get_record('role', ['shortname' => 'student'], '*', MUST_EXIST);
|
||||
$generator->enrol_user($student1->id, $course->id, $role->id);
|
||||
$generator->enrol_user($student2->id, $course->id, $role->id);
|
||||
$generator->enrol_user($student3->id, $course->id, $role->id);
|
||||
$generator->enrol_user($student4->id, $course->id, $role->id);
|
||||
$generator->enrol_user($student5->id, $course->id, $role->id, 'manual', 0, 0, ENROL_USER_SUSPENDED);
|
||||
|
||||
$group1 = $generator->create_group(['courseid' => $course->id]);
|
||||
$group2 = $generator->create_group(['courseid' => $course->id]);
|
||||
|
||||
$generator->create_group_member(['userid' => $student1->id, 'groupid' => $group1->id]);
|
||||
$generator->create_group_member(['userid' => $student1->id, 'groupid' => $group2->id]);
|
||||
$generator->create_group_member(['userid' => $student2->id, 'groupid' => $group2->id]);
|
||||
$generator->create_group_member(['userid' => $student3->id, 'groupid' => $group2->id]);
|
||||
|
||||
$DB->set_field('course', 'groupmode', SEPARATEGROUPS, ['id' => $course->id]);
|
||||
|
||||
$teacher = $generator->create_user(['username' => 'teacher1']);
|
||||
$role = $DB->get_record('role', ['shortname' => 'editingteacher'], '*', MUST_EXIST);
|
||||
$generator->enrol_user($teacher->id, $course->id, $role->id);
|
||||
|
||||
$generator->create_module('assign', ['course' => $course->id]);
|
||||
|
||||
$groupid = $grouprestricted ? $group2->id : 0;
|
||||
$result = get_gradable_users::execute($course->id, $groupid, $onlyactiveenrol);
|
||||
$result = external_api::clean_returnvalue(get_gradable_users::execute_returns(), $result);
|
||||
|
||||
$mapped = array_map(function($user) {
|
||||
return [
|
||||
'fullname' => $user['fullname'],
|
||||
'firstname' => $user['firstname'],
|
||||
'lastname' => $user['lastname'],
|
||||
'profileimageurl' => $user['profileimageurl'],
|
||||
];
|
||||
}, array_values($result['users']));
|
||||
$this->assertEquals($expected, $mapped);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_execute.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function execute_data(): array {
|
||||
return [
|
||||
'All users' => [
|
||||
false,
|
||||
false,
|
||||
[
|
||||
[
|
||||
'profileimageurl' => 'https://www.example.com/moodle/theme/image.php/_s/boost/core/1/u/f1',
|
||||
'firstname' => 'Apple',
|
||||
'lastname' => 'Apricot',
|
||||
'fullname' => 'Apple Apricot',
|
||||
],
|
||||
[
|
||||
'profileimageurl' => 'https://www.example.com/moodle/theme/image.php/_s/boost/core/1/u/f1',
|
||||
'firstname' => 'Banana',
|
||||
'lastname' => 'Blueberry',
|
||||
'fullname' => 'Banana Blueberry',
|
||||
],
|
||||
[
|
||||
'profileimageurl' => 'https://www.example.com/moodle/theme/image.php/_s/boost/core/1/u/f1',
|
||||
'firstname' => 'Cherry',
|
||||
'lastname' => 'Cranberry',
|
||||
'fullname' => 'Cherry Cranberry',
|
||||
],
|
||||
[
|
||||
'profileimageurl' => 'https://www.example.com/moodle/theme/image.php/_s/boost/core/1/u/f1',
|
||||
'firstname' => 'Durian',
|
||||
'lastname' => 'Dracontomelon',
|
||||
'fullname' => 'Durian Dracontomelon',
|
||||
],
|
||||
[
|
||||
'profileimageurl' => 'https://www.example.com/moodle/theme/image.php/_s/boost/core/1/u/f1',
|
||||
'firstname' => 'Eggplant',
|
||||
'lastname' => 'Ensete',
|
||||
'fullname' => 'Eggplant Ensete',
|
||||
],
|
||||
],
|
||||
],
|
||||
'Only active enrolment' => [
|
||||
true,
|
||||
false,
|
||||
[
|
||||
[
|
||||
'profileimageurl' => 'https://www.example.com/moodle/theme/image.php/_s/boost/core/1/u/f1',
|
||||
'firstname' => 'Apple',
|
||||
'lastname' => 'Apricot',
|
||||
'fullname' => 'Apple Apricot',
|
||||
],
|
||||
[
|
||||
'profileimageurl' => 'https://www.example.com/moodle/theme/image.php/_s/boost/core/1/u/f1',
|
||||
'firstname' => 'Banana',
|
||||
'lastname' => 'Blueberry',
|
||||
'fullname' => 'Banana Blueberry',
|
||||
],
|
||||
[
|
||||
'profileimageurl' => 'https://www.example.com/moodle/theme/image.php/_s/boost/core/1/u/f1',
|
||||
'firstname' => 'Cherry',
|
||||
'lastname' => 'Cranberry',
|
||||
'fullname' => 'Cherry Cranberry',
|
||||
],
|
||||
[
|
||||
'profileimageurl' => 'https://www.example.com/moodle/theme/image.php/_s/boost/core/1/u/f1',
|
||||
'firstname' => 'Durian',
|
||||
'lastname' => 'Dracontomelon',
|
||||
'fullname' => 'Durian Dracontomelon',
|
||||
],
|
||||
],
|
||||
],
|
||||
'Group restricted' => [
|
||||
false,
|
||||
true,
|
||||
[
|
||||
[
|
||||
'profileimageurl' => 'https://www.example.com/moodle/theme/image.php/_s/boost/core/1/u/f1',
|
||||
'firstname' => 'Apple',
|
||||
'lastname' => 'Apricot',
|
||||
'fullname' => 'Apple Apricot',
|
||||
],
|
||||
[
|
||||
'profileimageurl' => 'https://www.example.com/moodle/theme/image.php/_s/boost/core/1/u/f1',
|
||||
'firstname' => 'Banana',
|
||||
'lastname' => 'Blueberry',
|
||||
'fullname' => 'Banana Blueberry',
|
||||
],
|
||||
[
|
||||
'profileimageurl' => 'https://www.example.com/moodle/theme/image.php/_s/boost/core/1/u/f1',
|
||||
'firstname' => 'Cherry',
|
||||
'lastname' => 'Cranberry',
|
||||
'fullname' => 'Cherry Cranberry',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
First name,Last name,Email address,Manual item 1
|
||||
Student,1,student1@example.com,400
|
||||
|
@@ -0,0 +1,2 @@
|
||||
First name,Last name,Email address,Manual item 1
|
||||
Student,1,student1@example.com,800
|
||||
|
@@ -0,0 +1,2 @@
|
||||
First name,Last name,Email address,Manual item 1
|
||||
Student,1,student1@example.com,5
|
||||
|
@@ -751,7 +751,7 @@ class lib_test extends \advanced_testcase {
|
||||
// Now, let's suspend the enrolment of student2.
|
||||
$this->getDataGenerator()->enrol_user($student2->id, $course->id, 'student', 'manual', 0, 0, ENROL_USER_SUSPENDED);
|
||||
// Should return only the active gradable users (student1 and student3).
|
||||
$gradableusers = get_gradable_users($course->id);
|
||||
$gradableusers = \grade_report::get_gradable_users($course->id);
|
||||
$this->assertEqualsCanonicalizing([$student1->id, $student3->id], array_keys($gradableusers));
|
||||
|
||||
// Give teacher 'viewsuspendedusers' capability and set a preference to display suspended users.
|
||||
@@ -761,17 +761,17 @@ class lib_test extends \advanced_testcase {
|
||||
|
||||
$this->setUser($teacher);
|
||||
// Should return all gradable users (including suspended enrolments).
|
||||
$gradableusers = get_gradable_users($course->id);
|
||||
$gradableusers = \grade_report::get_gradable_users($course->id);
|
||||
$this->assertEqualsCanonicalizing([$student1->id, $student2->id, $student3->id], array_keys($gradableusers));
|
||||
|
||||
// Reactivate the course enrolment of student2.
|
||||
$this->getDataGenerator()->enrol_user($student2->id, $course->id, 'student', 'manual', 0, 0, ENROL_USER_ACTIVE);
|
||||
$this->setAdminUser();
|
||||
// Should return all gradable users from group1 (student1 and student2).
|
||||
$gradableusers = get_gradable_users($course->id, $group1->id);
|
||||
$gradableusers = \grade_report::get_gradable_users($course->id, $group1->id);
|
||||
$this->assertEqualsCanonicalizing([$student1->id, $student2->id], array_keys($gradableusers));
|
||||
// Should return all gradable users from group2 (student3).
|
||||
$gradableusers = get_gradable_users($course->id, $group2->id);
|
||||
$gradableusers = \grade_report::get_gradable_users($course->id, $group2->id);
|
||||
$this->assertEqualsCanonicalizing([$student3->id], array_keys($gradableusers));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
This file describes API changes in /grade/* ;
|
||||
Information provided here is intended especially for developers.
|
||||
|
||||
=== 4.1.7 ===
|
||||
* Some changes to how print_grade_page_head() sets the page title:
|
||||
* If a non-empty `$heading` parameter is passed, it will be used as the page title's unique identifying information.
|
||||
* Add support for grade report plugins that define an `editingmode_title` lang string. If the grade report plugin defines this
|
||||
language string, it will be used for the page title's unique identifying information when editing mode is turned on.
|
||||
* The function gradeimport_csv_load_data::insert_grade_record() now has extra parameter $gradeitem to carry over the grade item related info
|
||||
for validation.
|
||||
* The function get_gradable_users() in grade/lib.php has extra param now to retrieve only active enrolments.
|
||||
* A new webservice core_grades_get_gradable_users has been added to retrieve gradable users for a course.
|
||||
|
||||
=== 4.1.6 ===
|
||||
* The grade `itemname` property contained in the return structure of the following external methods is now PARAM_CLEANHTML:
|
||||
- `gradereport_user_get_grade_items`
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user